Massive architectural rework
This commit massively overhauls the project's structure to simplify development. Most parts are now correctly compartmentalized and dependencies are passed in a sane way rather than global variables galore xd.
This commit is contained in:
14
cmd/cli/commands/command.go
Normal file
14
cmd/cli/commands/command.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/juls0730/flux/pkg"
|
||||
"github.com/juls0730/flux/pkg/API"
|
||||
)
|
||||
|
||||
type CommandCtx struct {
|
||||
Config pkg.CLIConfig
|
||||
Info API.Info
|
||||
Interactive bool
|
||||
}
|
||||
|
||||
type CommandFunc func(CommandCtx, []string) error
|
||||
117
cmd/cli/commands/delete.go
Normal file
117
cmd/cli/commands/delete.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
util "github.com/juls0730/flux/internal/util/cli"
|
||||
)
|
||||
|
||||
var deleteUsage = `Usage:
|
||||
flux delete [project-name | all]
|
||||
|
||||
Options:
|
||||
project-name: The name of the project to delete
|
||||
all: Delete all projects
|
||||
|
||||
Flags:
|
||||
%s
|
||||
|
||||
Flux will delete the deployment of the app in the current directory or the specified project.`
|
||||
|
||||
func deleteAll(ctx CommandCtx, noConfirm *bool) error {
|
||||
if !*noConfirm {
|
||||
if !ctx.Interactive {
|
||||
return fmt.Errorf("delete command cannot be run non-interactively without --no-confirm")
|
||||
}
|
||||
|
||||
var response string
|
||||
fmt.Print("Are you sure you want to delete all projects? this will delete all volumes and containers associated and cannot be undone. [y/N] ")
|
||||
fmt.Scanln(&response)
|
||||
|
||||
if strings.ToLower(response) != "y" {
|
||||
fmt.Println("Aborting...")
|
||||
return nil
|
||||
}
|
||||
|
||||
response = ""
|
||||
|
||||
// since we are deleting **all** projects, I feel better asking for confirmation twice
|
||||
fmt.Printf("Are you really sure you want to delete all projects? [y/N] ")
|
||||
fmt.Scanln(&response)
|
||||
|
||||
if strings.ToLower(response) != "y" {
|
||||
fmt.Println("Aborting...")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
util.DeleteRequest(ctx.Config.DaemonURL + "/deployments")
|
||||
|
||||
fmt.Printf("Successfully deleted all projects\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteCommand(ctx CommandCtx, args []string) error {
|
||||
fs := flag.NewFlagSet("delete", flag.ExitOnError)
|
||||
fs.Usage = func() {
|
||||
var buf bytes.Buffer
|
||||
// Redirect flagset to print to buffer instead of stdout
|
||||
fs.SetOutput(&buf)
|
||||
fs.PrintDefaults()
|
||||
|
||||
fmt.Println(deleteUsage, strings.TrimRight(buf.String(), "\n"))
|
||||
}
|
||||
|
||||
noConfirm := fs.Bool("no-confirm", false, "Skip confirmation prompt")
|
||||
|
||||
err := fs.Parse(args)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
args = fs.Args()
|
||||
|
||||
if len(args) == 1 && args[0] == "all" {
|
||||
return deleteAll(ctx, noConfirm)
|
||||
}
|
||||
|
||||
project, err := util.GetProject("delete", args, ctx.Config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("\tfailed to get project name: %v.\n\tSee flux delete --help for more information", err)
|
||||
}
|
||||
|
||||
// ask for confirmation if not --no-confirm
|
||||
if !*noConfirm {
|
||||
if !ctx.Interactive {
|
||||
return fmt.Errorf("delete command cannot be run non-interactively without --no-confirm")
|
||||
}
|
||||
|
||||
fmt.Printf("Are you sure you want to delete %s? this will delete all volumes and containers associated with the deployment, and cannot be undone. \n[y/N] ", project.Name)
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
|
||||
if strings.ToLower(response) != "y" {
|
||||
fmt.Println("Aborting...")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
err = util.DeleteRequest(ctx.Config.DaemonURL + "/app/" + project.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete project: %v", err)
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
// remove the .fluxid file if it exists
|
||||
os.Remove(".fluxid")
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully deleted %s\n", project.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
372
cmd/cli/commands/deploy.go
Normal file
372
cmd/cli/commands/deploy.go
Normal file
@@ -0,0 +1,372 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/google/uuid"
|
||||
"github.com/joho/godotenv"
|
||||
util "github.com/juls0730/flux/internal/util/cli"
|
||||
"github.com/juls0730/flux/pkg"
|
||||
"github.com/juls0730/flux/pkg/API"
|
||||
)
|
||||
|
||||
func matchesIgnorePattern(path string, info os.FileInfo, patterns []string) bool {
|
||||
normalizedPath := filepath.ToSlash(path)
|
||||
normalizedPath = strings.TrimPrefix(normalizedPath, "./")
|
||||
|
||||
for _, pattern := range patterns {
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
if pattern == "" || strings.HasPrefix(pattern, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
regexPattern := convertGitignorePatternToRegex(pattern)
|
||||
|
||||
matched, err := regexp.MatchString(regexPattern, normalizedPath)
|
||||
if err == nil && matched {
|
||||
if strings.HasSuffix(pattern, "/") && info.IsDir() {
|
||||
return true
|
||||
}
|
||||
if !info.IsDir() {
|
||||
dir := filepath.Dir(normalizedPath)
|
||||
for dir != "." && dir != "/" {
|
||||
dirPattern := convertGitignorePatternToRegex(pattern)
|
||||
if matched, _ := regexp.MatchString(dirPattern, filepath.ToSlash(dir)); matched {
|
||||
return true
|
||||
}
|
||||
dir = filepath.Dir(dir)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func convertGitignorePatternToRegex(pattern string) string {
|
||||
pattern = strings.TrimSuffix(pattern, "/")
|
||||
pattern = regexp.QuoteMeta(pattern)
|
||||
pattern = strings.ReplaceAll(pattern, "\\*\\*", ".*")
|
||||
pattern = strings.ReplaceAll(pattern, "\\*", "[^/]*")
|
||||
pattern = strings.ReplaceAll(pattern, "\\?", ".")
|
||||
pattern = "(^|.*/)" + pattern + "(/.*)?$"
|
||||
|
||||
return pattern
|
||||
}
|
||||
|
||||
func compressDirectory(compressionLevel int) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
var err error
|
||||
|
||||
var ignoredFiles []string
|
||||
fluxIgnore, err := os.Open(".fluxignore")
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if fluxIgnore != nil {
|
||||
defer fluxIgnore.Close()
|
||||
|
||||
scanner := bufio.NewScanner(fluxIgnore)
|
||||
for scanner.Scan() {
|
||||
ignoredFiles = append(ignoredFiles, scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
var gzWriter *gzip.Writer
|
||||
if compressionLevel > 0 {
|
||||
gzWriter, err = gzip.NewWriterLevel(&buf, compressionLevel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var tarWriter *tar.Writer
|
||||
if gzWriter != nil {
|
||||
tarWriter = tar.NewWriter(gzWriter)
|
||||
} else {
|
||||
tarWriter = tar.NewWriter(&buf)
|
||||
}
|
||||
|
||||
err = filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if path == "flux.json" || info.IsDir() || matchesIgnorePattern(path, info, ignoredFiles) {
|
||||
return nil
|
||||
}
|
||||
|
||||
header, err := tar.FileInfoHeader(info, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = path
|
||||
|
||||
if err = tarWriter.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err = io.Copy(tarWriter, file); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = tarWriter.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gzWriter != nil {
|
||||
if err = gzWriter.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func preprocessEnvFile(envFile string, target *[]string) error {
|
||||
envBytes, err := os.Open(envFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open env file: %v", err)
|
||||
}
|
||||
defer envBytes.Close()
|
||||
|
||||
envVars, err := godotenv.Parse(envBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse env file: %v", err)
|
||||
}
|
||||
|
||||
for key, value := range envVars {
|
||||
*target = append(*target, fmt.Sprintf("%s=%s", key, value))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeployCommand(ctx CommandCtx, args []string) error {
|
||||
if _, err := os.Stat("flux.json"); err != nil {
|
||||
return fmt.Errorf("no flux.json found, please run flux init first")
|
||||
}
|
||||
|
||||
spinnerWriter := util.NewCustomSpinnerWriter()
|
||||
|
||||
loadingSpinner := spinner.New(spinner.CharSets[14], 100*time.Millisecond, spinner.WithWriter(spinnerWriter))
|
||||
defer func() {
|
||||
if loadingSpinner.Active() {
|
||||
loadingSpinner.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
signalChannel := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChannel, os.Interrupt)
|
||||
go func() {
|
||||
<-signalChannel
|
||||
if loadingSpinner.Active() {
|
||||
loadingSpinner.Stop()
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
loadingSpinner.Suffix = " Deploying"
|
||||
loadingSpinner.Start()
|
||||
|
||||
buf, err := compressDirectory(ctx.Info.CompressionLevel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compress directory: %v", err)
|
||||
}
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
if _, err := os.Stat(".fluxid"); err == nil {
|
||||
idPart, err := writer.CreateFormField("id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create id part: %v", err)
|
||||
}
|
||||
|
||||
idFile, err := os.Open(".fluxid")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open .fluxid: %v", err)
|
||||
}
|
||||
defer idFile.Close()
|
||||
|
||||
var idBytes []byte
|
||||
if idBytes, err = io.ReadAll(idFile); err != nil {
|
||||
return fmt.Errorf("failed to read .fluxid: %v", err)
|
||||
}
|
||||
|
||||
if _, err := uuid.Parse(string(idBytes)); err != nil {
|
||||
return fmt.Errorf(".fluxid does not contain a valid uuid")
|
||||
}
|
||||
|
||||
idPart.Write(idBytes)
|
||||
}
|
||||
|
||||
configPart, err := writer.CreateFormField("config")
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create config part: %v", err)
|
||||
}
|
||||
|
||||
type FluxContainers struct {
|
||||
pkg.Container
|
||||
EnvFile string `json:"env_file,omitempty"`
|
||||
}
|
||||
|
||||
type FluxConfig struct {
|
||||
pkg.ProjectConfig
|
||||
EnvFile string `json:"env_file,omitempty"`
|
||||
Containers []FluxContainers `json:"containers,omitempty"`
|
||||
}
|
||||
|
||||
fluxConfigFile, err := os.Open("flux.json")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open flux.json: %v", err)
|
||||
}
|
||||
defer fluxConfigFile.Close()
|
||||
|
||||
// Read the entire JSON file into a byte slice
|
||||
byteValue, err := io.ReadAll(fluxConfigFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read flux.json: %v", err)
|
||||
}
|
||||
|
||||
var fluxConfig FluxConfig
|
||||
err = json.Unmarshal(byteValue, &fluxConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal flux.json: %v", err)
|
||||
}
|
||||
|
||||
if fluxConfig.EnvFile != "" {
|
||||
if err := preprocessEnvFile(fluxConfig.EnvFile, &fluxConfig.Environment); err != nil {
|
||||
return fmt.Errorf("failed to preprocess env file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, container := range fluxConfig.Containers {
|
||||
if container.EnvFile != "" {
|
||||
if err := preprocessEnvFile(container.EnvFile, &container.Environment); err != nil {
|
||||
return fmt.Errorf("failed to preprocess env file: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// write the pre-processed flux.json to the config part
|
||||
if err := json.NewEncoder(configPart).Encode(fluxConfig); err != nil {
|
||||
return fmt.Errorf("failed to encode flux.json: %v", err)
|
||||
}
|
||||
|
||||
var codeFileName string
|
||||
if ctx.Info.CompressionLevel > 0 {
|
||||
codeFileName = "code.tar.gz"
|
||||
} else {
|
||||
codeFileName = "code.tar"
|
||||
}
|
||||
|
||||
codePart, err := writer.CreateFormFile("code", codeFileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create code part: %v", err)
|
||||
}
|
||||
|
||||
if _, err := codePart.Write(buf); err != nil {
|
||||
return fmt.Errorf("failed to write code part: %v", err)
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close writer: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", ctx.Config.DaemonURL+"/deploy", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
customWriter := util.NewCustomStdout(spinnerWriter)
|
||||
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
var event string
|
||||
var data API.DeploymentEvent
|
||||
var line string
|
||||
for scanner.Scan() {
|
||||
line = scanner.Text()
|
||||
if strings.HasPrefix(line, "data: ") {
|
||||
if err := json.Unmarshal([]byte(line[6:]), &data); err != nil {
|
||||
return fmt.Errorf("failed to parse deployment event: %v", err)
|
||||
}
|
||||
|
||||
switch event {
|
||||
case "complete":
|
||||
loadingSpinner.Stop()
|
||||
fmt.Printf("App %s deployed successfully!\n", data.Message.(map[string]any)["name"])
|
||||
if _, err := os.Stat(".fluxid"); os.IsNotExist(err) {
|
||||
idFile, err := os.Create(".fluxid")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create .fluxid: %v", err)
|
||||
}
|
||||
defer idFile.Close()
|
||||
|
||||
id := data.Message.(map[string]any)["id"].(string)
|
||||
if _, err := idFile.Write([]byte(id)); err != nil {
|
||||
return fmt.Errorf("failed to write .fluxid: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case "cmd_output":
|
||||
customWriter.Printf("... %s\n", data.Message)
|
||||
case "error":
|
||||
loadingSpinner.Stop()
|
||||
return fmt.Errorf("deployment failed: %s", data.Message)
|
||||
default:
|
||||
customWriter.Printf("%s\n", data.Message)
|
||||
}
|
||||
event = ""
|
||||
} else if strings.HasPrefix(line, "event: ") {
|
||||
event = strings.TrimPrefix(line, "event: ")
|
||||
}
|
||||
}
|
||||
|
||||
// the stream closed, but we didnt get a "complete" event
|
||||
line = strings.TrimSuffix(line, "\n")
|
||||
return fmt.Errorf("deploy failed: %s", line)
|
||||
}
|
||||
92
cmd/cli/commands/init.go
Normal file
92
cmd/cli/commands/init.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/juls0730/flux/pkg"
|
||||
)
|
||||
|
||||
var initUsage = `Usage:
|
||||
flux init [project-name]
|
||||
|
||||
Options:
|
||||
project-name: The name of the project to initialize
|
||||
|
||||
Flux will initialize a new project in the current directory or the specified project.`
|
||||
|
||||
func InitCommand(ctx CommandCtx, args []string) error {
|
||||
if !ctx.Interactive {
|
||||
return fmt.Errorf("init command can only be run in interactive mode")
|
||||
}
|
||||
|
||||
fs := flag.NewFlagSet("init", flag.ExitOnError)
|
||||
fs.Usage = func() {
|
||||
var buf bytes.Buffer
|
||||
// Redirect flagset to print to buffer instead of stdout
|
||||
fs.SetOutput(&buf)
|
||||
fs.PrintDefaults()
|
||||
|
||||
fmt.Println(initUsage)
|
||||
}
|
||||
|
||||
err := fs.Parse(args)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
args = fs.Args()
|
||||
|
||||
var projectConfig pkg.ProjectConfig
|
||||
|
||||
var response string
|
||||
if len(args) > 1 {
|
||||
response = args[0]
|
||||
} else {
|
||||
fmt.Println("What is the name of your project?")
|
||||
fmt.Scanln(&response)
|
||||
}
|
||||
|
||||
projectConfig.Name = response
|
||||
|
||||
fmt.Println("What URL should your project listen to?")
|
||||
fmt.Scanln(&response)
|
||||
if strings.HasPrefix(response, "http") {
|
||||
response = strings.TrimPrefix(response, "http://")
|
||||
response = strings.TrimPrefix(response, "https://")
|
||||
}
|
||||
|
||||
response = strings.Split(response, "/")[0]
|
||||
|
||||
projectConfig.Url = response
|
||||
|
||||
fmt.Println("What port does your project listen to?")
|
||||
fmt.Scanln(&response)
|
||||
port, err := strconv.ParseUint(response, 10, 16)
|
||||
portErr := fmt.Errorf("that doesnt look like a valid port, try a number between 1024 and 65535")
|
||||
if port > 65535 {
|
||||
return portErr
|
||||
}
|
||||
|
||||
projectConfig.Port = uint16(port)
|
||||
if err != nil || projectConfig.Port < 1024 {
|
||||
return portErr
|
||||
}
|
||||
|
||||
configBytes, err := json.MarshalIndent(projectConfig, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse project config: %v", err)
|
||||
}
|
||||
|
||||
os.WriteFile("flux.json", configBytes, 0644)
|
||||
|
||||
fmt.Printf("Successfully initialized project %s\n", projectConfig.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
26
cmd/cli/commands/list.go
Normal file
26
cmd/cli/commands/list.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
util "github.com/juls0730/flux/internal/util/cli"
|
||||
"github.com/juls0730/flux/pkg/API"
|
||||
)
|
||||
|
||||
func ListCommand(ctx CommandCtx, args []string) error {
|
||||
apps, err := util.GetRequest[[]API.App](ctx.Config.DaemonURL + "/apps")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get apps: %v", err)
|
||||
}
|
||||
|
||||
if len(*apps) == 0 {
|
||||
fmt.Println("No apps found")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, app := range *apps {
|
||||
fmt.Printf("%s (%s)\n", app.Name, app.DeploymentStatus)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
22
cmd/cli/commands/start.go
Normal file
22
cmd/cli/commands/start.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
util "github.com/juls0730/flux/internal/util/cli"
|
||||
)
|
||||
|
||||
func StartCommand(ctx CommandCtx, args []string) error {
|
||||
projectName, err := util.GetProject("start", args, ctx.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Put request to start the project, since the start endpoint is idempotent.
|
||||
// If the project is already running, this will return a 304 Not Modified
|
||||
util.PutRequest(ctx.Config.DaemonURL+"/app/"+projectName.Id+"/start", nil)
|
||||
|
||||
fmt.Printf("Successfully started %s\n", projectName)
|
||||
|
||||
return nil
|
||||
}
|
||||
19
cmd/cli/commands/stop.go
Normal file
19
cmd/cli/commands/stop.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
util "github.com/juls0730/flux/internal/util/cli"
|
||||
)
|
||||
|
||||
func StopCommand(ctx CommandCtx, args []string) error {
|
||||
projectName, err := util.GetProject("stop", args, ctx.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
util.PutRequest(ctx.Config.DaemonURL+"/app/"+projectName.Id+"/stop", nil)
|
||||
|
||||
fmt.Printf("Successfully stopped %s\n", projectName)
|
||||
return nil
|
||||
}
|
||||
3
cmd/cli/config.json
Normal file
3
cmd/cli/config.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"deamon_url": "http://127.0.0.1:5647"
|
||||
}
|
||||
233
cmd/cli/main.go
Normal file
233
cmd/cli/main.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/agnivade/levenshtein"
|
||||
"github.com/juls0730/flux/cmd/cli/commands"
|
||||
util "github.com/juls0730/flux/internal/util/cli"
|
||||
"github.com/juls0730/flux/pkg"
|
||||
"github.com/juls0730/flux/pkg/API"
|
||||
"github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
func isInteractive() bool {
|
||||
return isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
|
||||
}
|
||||
|
||||
//go:embed config.json
|
||||
var config []byte
|
||||
|
||||
var configPath = filepath.Join(os.Getenv("HOME"), "/.config/flux")
|
||||
|
||||
var version = pkg.Version
|
||||
|
||||
var helpStr = `Usage:
|
||||
flux <command>
|
||||
|
||||
Available Commands:
|
||||
%s
|
||||
|
||||
Available Flags:
|
||||
--help, -h: Show this help message
|
||||
|
||||
Use "flux <command> --help" for more information about a command.
|
||||
`
|
||||
|
||||
var maxDistance = 3
|
||||
|
||||
type Command struct {
|
||||
Help string
|
||||
HandlerFunc commands.CommandFunc
|
||||
}
|
||||
|
||||
type CommandHandler struct {
|
||||
commands map[string]Command
|
||||
aliases map[string]string
|
||||
}
|
||||
|
||||
func NewCommandHandler() CommandHandler {
|
||||
return CommandHandler{
|
||||
commands: make(map[string]Command),
|
||||
aliases: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CommandHandler) RegisterCmd(name string, handler commands.CommandFunc, help string) {
|
||||
coomand := Command{
|
||||
Help: help,
|
||||
HandlerFunc: handler,
|
||||
}
|
||||
|
||||
h.commands[name] = coomand
|
||||
}
|
||||
|
||||
func (h *CommandHandler) RegisterAlias(alias string, command string) {
|
||||
h.aliases[alias] = command
|
||||
}
|
||||
|
||||
// returns the command and whether or not it exists
|
||||
func (h *CommandHandler) GetCommand(command string) (Command, bool) {
|
||||
if command, ok := h.aliases[command]; ok {
|
||||
return h.commands[command], true
|
||||
}
|
||||
|
||||
commandStruct, ok := h.commands[command]
|
||||
return commandStruct, ok
|
||||
}
|
||||
|
||||
var helpPadding = 13
|
||||
|
||||
func (h *CommandHandler) GetHelp() {
|
||||
commandsStr := ""
|
||||
for command := range h.commands {
|
||||
curLine := ""
|
||||
|
||||
curLine += command
|
||||
for alias, aliasCommand := range h.aliases {
|
||||
if aliasCommand == command {
|
||||
curLine += fmt.Sprintf(", %s", alias)
|
||||
}
|
||||
}
|
||||
|
||||
curLine += strings.Repeat(" ", helpPadding-(len(curLine)-2))
|
||||
commandsStr += fmt.Sprintf(" %s %s\n", curLine, h.commands[command].Help)
|
||||
}
|
||||
|
||||
fmt.Printf(helpStr, strings.TrimRight(commandsStr, "\n"))
|
||||
}
|
||||
|
||||
func (h *CommandHandler) GetHelpCmd(commands.CommandCtx, []string) error {
|
||||
h.GetHelp()
|
||||
return nil
|
||||
}
|
||||
|
||||
func runCommand(command string, args []string, config pkg.CLIConfig, info API.Info, cmdHandler CommandHandler) error {
|
||||
commandCtx := commands.CommandCtx{
|
||||
Config: config,
|
||||
Info: info,
|
||||
Interactive: isInteractive(),
|
||||
}
|
||||
|
||||
commandStruct, ok := cmdHandler.commands[command]
|
||||
if ok {
|
||||
return commandStruct.HandlerFunc(commandCtx, args)
|
||||
}
|
||||
|
||||
// diff the command against the list of commands and if we find a command that is more than 80% similar, ask if that's what the user meant
|
||||
var closestMatch struct {
|
||||
name string
|
||||
score int
|
||||
}
|
||||
for cmdName := range cmdHandler.commands {
|
||||
distance := levenshtein.ComputeDistance(cmdName, command)
|
||||
|
||||
if distance <= maxDistance {
|
||||
if closestMatch.name == "" || distance < closestMatch.score {
|
||||
closestMatch.name = cmdName
|
||||
closestMatch.score = distance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if closestMatch.name == "" {
|
||||
return fmt.Errorf("unknown command: %s", command)
|
||||
}
|
||||
|
||||
var response string
|
||||
// new line ommitted because it will be produced when the user presses enter to submit their response
|
||||
fmt.Printf("No command found with the name '%s'. Did you mean '%s'? (y/N)", command, closestMatch.name)
|
||||
fmt.Scanln(&response)
|
||||
|
||||
if strings.ToLower(response) == "y" || strings.ToLower(response) == "yes" {
|
||||
command = closestMatch.name
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// re-run command after accepting the suggestion
|
||||
return runCommand(command, args, config, info, cmdHandler)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if !isInteractive() {
|
||||
fmt.Printf("Flux is being run non-interactively\n")
|
||||
}
|
||||
|
||||
cmdHandler := NewCommandHandler()
|
||||
|
||||
cmdHandler.RegisterCmd("init", commands.InitCommand, "Initialize a new project")
|
||||
cmdHandler.RegisterCmd("deploy", commands.DeployCommand, "Deploy a new version of the app")
|
||||
cmdHandler.RegisterCmd("start", commands.StartCommand, "Start the app")
|
||||
cmdHandler.RegisterCmd("stop", commands.StopCommand, "Stop the app")
|
||||
cmdHandler.RegisterCmd("list", commands.ListCommand, "List all the apps")
|
||||
cmdHandler.RegisterCmd("delete", commands.DeleteCommand, "Delete the app")
|
||||
|
||||
fs := flag.NewFlagSet("flux", flag.ExitOnError)
|
||||
fs.Usage = func() {
|
||||
cmdHandler.GetHelp()
|
||||
}
|
||||
|
||||
err := fs.Parse(os.Args[1:])
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(os.Args) < 2 {
|
||||
cmdHandler.GetHelp()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(configPath, "config.json")); err != nil {
|
||||
if err := os.MkdirAll(configPath, 0755); err != nil {
|
||||
fmt.Printf("Failed to create config directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = os.WriteFile(filepath.Join(configPath, "config.json"), config, 0644); err != nil {
|
||||
fmt.Printf("Failed to write config file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var config pkg.CLIConfig
|
||||
configBytes, err := os.ReadFile(filepath.Join(configPath, "config.json"))
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read config file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(configBytes, &config); err != nil {
|
||||
fmt.Printf("Failed to parse config file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if config.DaemonURL == "" {
|
||||
fmt.Printf("Daemon URL is empty\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
info, err := util.GetRequest[API.Info](config.DaemonURL + "/heartbeat")
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to connect to daemon\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if info.Version != version {
|
||||
fmt.Printf("Version mismatch, daemon is running version %s, but you are running version %s\n", info.Version, version)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = runCommand(os.Args[1], fs.Args()[1:], config, *info, cmdHandler)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user