improve cli code
This commit is contained in:
30
README.md
30
README.md
@@ -9,7 +9,7 @@ Flux is a lightweight self-hosted pseudo-PaaS for hosting Golang web apps with e
|
|||||||
- Simple but powerful configuration, flux should be able to handle most use cases, from a micro web app to a fullstack app with databases, caching layers, full text search, etc.
|
- Simple but powerful configuration, flux should be able to handle most use cases, from a micro web app to a fullstack app with databases, caching layers, full text search, etc.
|
||||||
|
|
||||||
**Limitations**:
|
**Limitations**:
|
||||||
- Theoretically only supports up to 1023 containers (roughly 500 apps assuming 2 containers per app), this is because flux uses the same bridge network for all containers (this could theoretically be increased if flux was smart enough to create new networks once we hit the max, but this is not a priority)
|
- Theoretically flux is likely limited by the amount of containers can fit in the bridge network, but I haven't tested this
|
||||||
- Containers are not particularly isolated, if one malicious container wanted to scan all containers, or interact with other containers it tectically shouldnt, it totally just can (todo?)
|
- Containers are not particularly isolated, if one malicious container wanted to scan all containers, or interact with other containers it tectically shouldnt, it totally just can (todo?)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -77,7 +77,6 @@ After=network.target
|
|||||||
ExecStart=/usr/local/bin/fluxd
|
ExecStart=/usr/local/bin/fluxd
|
||||||
Restart=always
|
Restart=always
|
||||||
Environment=GOPATH=/var/fluxd/go
|
Environment=GOPATH=/var/fluxd/go
|
||||||
Environment=HOME=/var/fluxd/home
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
@@ -150,18 +149,33 @@ flux.json is the configuration file in the root of your proejct that defines dep
|
|||||||
"name": "my-app",
|
"name": "my-app",
|
||||||
"url": "myapp.example.com",
|
"url": "myapp.example.com",
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
|
"containers": [
|
||||||
|
{
|
||||||
|
"name": "redis",
|
||||||
|
"image": "redis:latest",
|
||||||
|
"volumes": [
|
||||||
|
{
|
||||||
|
"mountpoint": "/data"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
"env_file": ".env",
|
"env_file": ".env",
|
||||||
"environment": ["DEBUG=true"]
|
"environment": ["DEBUG=true"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Configuration Options
|
The project config files has the following options:
|
||||||
|
|
||||||
- `name`: The name of the project
|
| field | description | required |
|
||||||
- `url`: Domain for the application
|
| ----- | ----------- | -------- |
|
||||||
- `port`: Web server's listening port
|
| `name` | The name of the project | true |
|
||||||
- `env_file`: Path to environment variable file
|
| `url` | Domain for the application | true |
|
||||||
- `environment`: Additional environment variables
|
| `port` | Web server's listening port | true |
|
||||||
|
| `env_file` | Path to environment variable file | false |
|
||||||
|
| `environment` | Additional environment variables | false |
|
||||||
|
| `containers` | Supplemental containers to run alongside the app | false |
|
||||||
|
| `volumes` | Volumes to mount to the app's containers | false |
|
||||||
|
|
||||||
## Deployment Notes
|
## Deployment Notes
|
||||||
|
|
||||||
|
|||||||
@@ -1,92 +1,121 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/briandowns/spinner"
|
|
||||||
"github.com/juls0730/flux/cmd/flux/models"
|
"github.com/juls0730/flux/cmd/flux/models"
|
||||||
"github.com/juls0730/flux/pkg"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func DeleteCommand(seekingHelp bool, config models.Config, info pkg.Info, loadingSpinner *spinner.Spinner, spinnerWriter *models.CustomSpinnerWriter, args []string) error {
|
var usage = `Usage:
|
||||||
if seekingHelp {
|
flux delete [project-name | all]
|
||||||
fmt.Println(`Usage:
|
|
||||||
flux delete [project-name | all]
|
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
project-name: The name of the project to delete
|
project-name: The name of the project to delete
|
||||||
all: Delete all projects
|
all: Delete all projects
|
||||||
|
|
||||||
Flux will delete the deployment of the app in the current directory or the specified project.`)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) == 1 {
|
Flags:
|
||||||
if args[0] == "all" {
|
%s
|
||||||
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. \n[y/N] ")
|
|
||||||
fmt.Scanln(&response)
|
|
||||||
|
|
||||||
if strings.ToLower(response) != "y" {
|
Flux will delete the deployment of the app in the current directory or the specified project.
|
||||||
fmt.Println("Aborting...")
|
`
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
response = ""
|
func deleteAll(ctx models.CommandCtx, noConfirm *bool) error {
|
||||||
|
if !*noConfirm {
|
||||||
|
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)
|
||||||
|
|
||||||
fmt.Printf("Are you really sure you want to delete all projects? \n[y/N] ")
|
if strings.ToLower(response) != "y" {
|
||||||
fmt.Scanln(&response)
|
fmt.Println("Aborting...")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if strings.ToLower(response) != "y" {
|
response = ""
|
||||||
fmt.Println("Aborting...")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("DELETE", config.DeamonURL+"/deployments", nil)
|
// since we are deleting **all** projects, I feel better asking for confirmation twice
|
||||||
if err != nil {
|
fmt.Printf("Are you really sure you want to delete all projects? [y/N] ")
|
||||||
return fmt.Errorf("failed to delete deployments: %v", err)
|
fmt.Scanln(&response)
|
||||||
}
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to delete deployments: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if strings.ToLower(response) != "y" {
|
||||||
responseBody, err := io.ReadAll(resp.Body)
|
fmt.Println("Aborting...")
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error reading response body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
responseBody = []byte(strings.TrimSuffix(string(responseBody), "\n"))
|
|
||||||
|
|
||||||
return fmt.Errorf("delete failed: %s", responseBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Successfully deleted all projects\n")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("DELETE", ctx.Config.DeamonURL+"/deployments", nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete deployments: %v", err)
|
||||||
|
}
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete deployments: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBody = []byte(strings.TrimSuffix(string(responseBody), "\n"))
|
||||||
|
|
||||||
|
return fmt.Errorf("delete failed: %s", responseBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Successfully deleted all projects\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteCommand(ctx models.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.Printf(usage, 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)
|
||||||
|
}
|
||||||
|
|
||||||
projectName, err := GetProjectName("delete", args)
|
projectName, err := GetProjectName("delete", args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("\tfailed to get project name: %v.\n\tSee flux delete --help for more information", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ask for confirmation
|
// ask for confirmation
|
||||||
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] ", projectName)
|
if !*noConfirm {
|
||||||
var response string
|
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] ", projectName)
|
||||||
fmt.Scanln(&response)
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
|
||||||
if strings.ToLower(response) != "y" {
|
if strings.ToLower(response) != "y" {
|
||||||
fmt.Println("Aborting...")
|
fmt.Println("Aborting...")
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("DELETE", config.DeamonURL+"/deployments/"+projectName, nil)
|
req, err := http.NewRequest("DELETE", ctx.Config.DeamonURL+"/deployments/"+projectName, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to delete app: %v", err)
|
return fmt.Errorf("failed to delete app: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -12,9 +11,11 @@ import (
|
|||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/briandowns/spinner"
|
"github.com/briandowns/spinner"
|
||||||
"github.com/juls0730/flux/cmd/flux/models"
|
"github.com/juls0730/flux/cmd/flux/models"
|
||||||
@@ -152,23 +153,35 @@ func compressDirectory(compression pkg.Compression) ([]byte, error) {
|
|||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeployCommand(seekingHelp bool, config models.Config, info pkg.Info, loadingSpinner *spinner.Spinner, spinnerWriter *models.CustomSpinnerWriter, args []string) error {
|
func DeployCommand(ctx models.CommandCtx, args []string) error {
|
||||||
if seekingHelp {
|
|
||||||
fmt.Println(`Usage:
|
|
||||||
flux deploy
|
|
||||||
|
|
||||||
Flux will deploy the app in the current directory, and start routing traffic to it.`)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat("flux.json"); err != nil {
|
if _, err := os.Stat("flux.json"); err != nil {
|
||||||
return fmt.Errorf("no flux.json found, please run flux init first")
|
return fmt.Errorf("no flux.json found, please run flux init first")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spinnerWriter := models.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.Suffix = " Deploying"
|
||||||
loadingSpinner.Start()
|
loadingSpinner.Start()
|
||||||
|
|
||||||
buf, err := compressDirectory(info.Compression)
|
buf, err := compressDirectory(ctx.Info.Compression)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to compress directory: %v", err)
|
return fmt.Errorf("failed to compress directory: %v", err)
|
||||||
}
|
}
|
||||||
@@ -204,7 +217,7 @@ func DeployCommand(seekingHelp bool, config models.Config, info pkg.Info, loadin
|
|||||||
return fmt.Errorf("failed to close writer: %v", err)
|
return fmt.Errorf("failed to close writer: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", config.DeamonURL+"/deploy", body)
|
req, err := http.NewRequest("POST", ctx.Config.DeamonURL+"/deploy", body)
|
||||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -7,22 +7,21 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/briandowns/spinner"
|
|
||||||
"github.com/juls0730/flux/cmd/flux/models"
|
"github.com/juls0730/flux/cmd/flux/models"
|
||||||
"github.com/juls0730/flux/pkg"
|
"github.com/juls0730/flux/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitCommand(seekingHelp bool, config models.Config, info pkg.Info, loadingSpinner *spinner.Spinner, spinnerWriter *models.CustomSpinnerWriter, args []string) error {
|
func InitCommand(ctx models.CommandCtx, args []string) error {
|
||||||
if seekingHelp {
|
// if seekingHelp {
|
||||||
fmt.Println(`Usage:
|
// fmt.Println(`Usage:
|
||||||
flux init [project-name]
|
// flux init [project-name]
|
||||||
|
|
||||||
Options:
|
// Options:
|
||||||
project-name: The name of the project to initialize
|
// project-name: The name of the project to initialize
|
||||||
|
|
||||||
Flux will initialize a new project in the current directory or the specified project.`)
|
// Flux will initialize a new project in the current directory or the specified project.`)
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
var projectConfig pkg.ProjectConfig
|
var projectConfig pkg.ProjectConfig
|
||||||
|
|
||||||
|
|||||||
@@ -7,21 +7,12 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/briandowns/spinner"
|
|
||||||
"github.com/juls0730/flux/cmd/flux/models"
|
"github.com/juls0730/flux/cmd/flux/models"
|
||||||
"github.com/juls0730/flux/pkg"
|
"github.com/juls0730/flux/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ListCommand(seekingHelp bool, config models.Config, info pkg.Info, loadingSpinner *spinner.Spinner, spinnerWriter *models.CustomSpinnerWriter, args []string) error {
|
func ListCommand(ctx models.CommandCtx, args []string) error {
|
||||||
if seekingHelp {
|
resp, err := http.Get(ctx.Config.DeamonURL + "/apps")
|
||||||
fmt.Println(`Usage:
|
|
||||||
flux list
|
|
||||||
|
|
||||||
Flux will list all the apps in the daemon.`)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := http.Get(config.DeamonURL + "/apps")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get apps: %v", err)
|
return fmt.Errorf("failed to get apps: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func GetProjectName(command string, args []string) (string, error) {
|
|||||||
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
if _, err := os.Stat("flux.json"); err != nil {
|
if _, err := os.Stat("flux.json"); err != nil {
|
||||||
return "", fmt.Errorf("usage: flux %[1]s <app name>, or run flux %[1]s in the project directory", command)
|
return "", fmt.Errorf("the current directory is not a flux project, please run flux %[1]s in the project directory", command)
|
||||||
}
|
}
|
||||||
|
|
||||||
fluxConfigFile, err := os.Open("flux.json")
|
fluxConfigFile, err := os.Open("flux.json")
|
||||||
|
|||||||
@@ -6,26 +6,16 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/briandowns/spinner"
|
|
||||||
"github.com/juls0730/flux/cmd/flux/models"
|
"github.com/juls0730/flux/cmd/flux/models"
|
||||||
"github.com/juls0730/flux/pkg"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func StartCommand(seekingHelp bool, config models.Config, info pkg.Info, loadingSpinner *spinner.Spinner, spinnerWriter *models.CustomSpinnerWriter, args []string) error {
|
func StartCommand(ctx models.CommandCtx, args []string) error {
|
||||||
if seekingHelp {
|
|
||||||
fmt.Println(`Usage:
|
|
||||||
flux start
|
|
||||||
|
|
||||||
Flux will start the deployment of the app in the current directory.`)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
projectName, err := GetProjectName("start", args)
|
projectName, err := GetProjectName("start", args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.Post(config.DeamonURL+"/start/"+projectName, "application/json", nil)
|
req, err := http.Post(ctx.Config.DeamonURL+"/start/"+projectName, "application/json", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to start app: %v", err)
|
return fmt.Errorf("failed to start app: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,26 +6,16 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/briandowns/spinner"
|
|
||||||
"github.com/juls0730/flux/cmd/flux/models"
|
"github.com/juls0730/flux/cmd/flux/models"
|
||||||
"github.com/juls0730/flux/pkg"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func StopCommand(seekingHelp bool, config models.Config, info pkg.Info, loadingSpinner *spinner.Spinner, spinnerWriter *models.CustomSpinnerWriter, args []string) error {
|
func StopCommand(ctx models.CommandCtx, args []string) error {
|
||||||
if seekingHelp {
|
|
||||||
fmt.Println(`Usage:
|
|
||||||
flux stop
|
|
||||||
|
|
||||||
Flux will stop the deployment of the app in the current directory.`)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
projectName, err := GetProjectName("stop", args)
|
projectName, err := GetProjectName("stop", args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.Post(config.DeamonURL+"/stop/"+projectName, "application/json", nil)
|
req, err := http.Post(ctx.Config.DeamonURL+"/stop/"+projectName, "application/json", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to stop app: %v", err)
|
return fmt.Errorf("failed to stop app: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
155
cmd/flux/main.go
155
cmd/flux/main.go
@@ -3,16 +3,14 @@ package main
|
|||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/agnivade/levenshtein"
|
"github.com/agnivade/levenshtein"
|
||||||
"github.com/briandowns/spinner"
|
|
||||||
"github.com/juls0730/flux/cmd/flux/commands"
|
"github.com/juls0730/flux/cmd/flux/commands"
|
||||||
"github.com/juls0730/flux/cmd/flux/models"
|
"github.com/juls0730/flux/cmd/flux/models"
|
||||||
"github.com/juls0730/flux/pkg"
|
"github.com/juls0730/flux/pkg"
|
||||||
@@ -29,62 +27,93 @@ var helpStr = `Usage:
|
|||||||
flux <command>
|
flux <command>
|
||||||
|
|
||||||
Available Commands:
|
Available Commands:
|
||||||
init Initialize a new project
|
%s
|
||||||
deploy Deploy a new version of the app
|
|
||||||
stop Stop a container
|
|
||||||
start Start a container
|
|
||||||
delete Delete a container
|
|
||||||
list List all containers
|
|
||||||
|
|
||||||
Flags:
|
Available Flags:
|
||||||
-h, --help help for flux
|
--help, -h: Show this help message
|
||||||
|
|
||||||
Use "flux <command> --help" for more information about a command.`
|
Use "flux <command> --help" for more information about a command.
|
||||||
|
`
|
||||||
|
|
||||||
var maxDistance = 3
|
var maxDistance = 3
|
||||||
|
|
||||||
|
type CommandFunc func(models.CommandCtx, []string) error
|
||||||
|
|
||||||
|
type Command struct {
|
||||||
|
Help string
|
||||||
|
HandlerFunc CommandFunc
|
||||||
|
}
|
||||||
|
|
||||||
type CommandHandler struct {
|
type CommandHandler struct {
|
||||||
commands map[string]func(bool, models.Config, pkg.Info, *spinner.Spinner, *models.CustomSpinnerWriter, []string) error
|
commands map[string]Command
|
||||||
|
aliases map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *CommandHandler) RegisterCmd(name string, handler func(bool, models.Config, pkg.Info, *spinner.Spinner, *models.CustomSpinnerWriter, []string) error) {
|
func NewCommandHandler() CommandHandler {
|
||||||
h.commands[name] = handler
|
return CommandHandler{
|
||||||
|
commands: make(map[string]Command),
|
||||||
|
aliases: make(map[string]string),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCommand(command string, args []string, config models.Config, info pkg.Info, cmdHandler CommandHandler, try int) error {
|
func (h *CommandHandler) RegisterCmd(name string, handler CommandFunc, help string) {
|
||||||
if try == 2 {
|
coomand := Command{
|
||||||
return fmt.Errorf("unknown command: %s", command)
|
Help: help,
|
||||||
|
HandlerFunc: handler,
|
||||||
}
|
}
|
||||||
|
|
||||||
seekingHelp := false
|
h.commands[name] = coomand
|
||||||
if len(args) > 0 && (args[len(args)-1] == "--help" || args[len(args)-1] == "-h") {
|
}
|
||||||
seekingHelp = true
|
|
||||||
args = args[:len(args)-1]
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
spinnerWriter := models.NewCustomSpinnerWriter()
|
commandStruct, ok := h.commands[command]
|
||||||
|
return commandStruct, ok
|
||||||
|
}
|
||||||
|
|
||||||
loadingSpinner := spinner.New(spinner.CharSets[14], 100*time.Millisecond, spinner.WithWriter(spinnerWriter))
|
var helpPadding = 13
|
||||||
defer func() {
|
|
||||||
if loadingSpinner.Active() {
|
|
||||||
loadingSpinner.Stop()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
signalChannel := make(chan os.Signal, 1)
|
func (h *CommandHandler) GetHelp() {
|
||||||
signal.Notify(signalChannel, os.Interrupt)
|
commandsStr := ""
|
||||||
go func() {
|
for command := range h.commands {
|
||||||
<-signalChannel
|
curLine := ""
|
||||||
if loadingSpinner.Active() {
|
|
||||||
loadingSpinner.Stop()
|
curLine += command
|
||||||
|
for alias, aliasCommand := range h.aliases {
|
||||||
|
if aliasCommand == command {
|
||||||
|
curLine += fmt.Sprintf(", %s", alias)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
curLine += strings.Repeat(" ", helpPadding-(len(curLine)-2))
|
||||||
}()
|
commandsStr += fmt.Sprintf(" %s %s\n", curLine, h.commands[command].Help)
|
||||||
|
}
|
||||||
|
|
||||||
handler, ok := cmdHandler.commands[command]
|
fmt.Printf(helpStr, strings.TrimRight(commandsStr, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CommandHandler) GetHelpCmd(models.CommandCtx, []string) error {
|
||||||
|
h.GetHelp()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCommand(command string, args []string, config models.Config, info pkg.Info, cmdHandler CommandHandler) error {
|
||||||
|
commandCtx := models.CommandCtx{
|
||||||
|
Config: config,
|
||||||
|
Info: info,
|
||||||
|
}
|
||||||
|
|
||||||
|
commandStruct, ok := cmdHandler.commands[command]
|
||||||
if ok {
|
if ok {
|
||||||
return handler(seekingHelp, config, info, loadingSpinner, spinnerWriter, args)
|
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
|
// 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
|
||||||
@@ -108,7 +137,8 @@ func runCommand(command string, args []string, config models.Config, info pkg.In
|
|||||||
}
|
}
|
||||||
|
|
||||||
var response string
|
var response string
|
||||||
fmt.Printf("No command found with the name '%s'. Did you mean '%s'?\n", command, closestMatch.name)
|
// 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)
|
fmt.Scanln(&response)
|
||||||
|
|
||||||
if strings.ToLower(response) == "y" || strings.ToLower(response) == "yes" {
|
if strings.ToLower(response) == "y" || strings.ToLower(response) == "yes" {
|
||||||
@@ -117,18 +147,34 @@ func runCommand(command string, args []string, config models.Config, info pkg.In
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return runCommand(command, args, config, info, cmdHandler, try+1)
|
// re-run command after accepting the suggestion
|
||||||
|
return runCommand(command, args, config, info, cmdHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
cmdHandler := NewCommandHandler()
|
||||||
fmt.Println(helpStr)
|
|
||||||
|
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.Args[1] == "--help" || os.Args[1] == "-h" {
|
if len(os.Args) < 2 {
|
||||||
fmt.Println(helpStr)
|
cmdHandler.GetHelp()
|
||||||
os.Exit(0)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(filepath.Join(configPath, "config.json")); err != nil {
|
if _, err := os.Stat(filepath.Join(configPath, "config.json")); err != nil {
|
||||||
@@ -155,9 +201,6 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
command := os.Args[1]
|
|
||||||
args := os.Args[2:]
|
|
||||||
|
|
||||||
resp, err := http.Get(config.DeamonURL + "/heartbeat")
|
resp, err := http.Get(config.DeamonURL + "/heartbeat")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to connect to daemon")
|
fmt.Println("Failed to connect to daemon")
|
||||||
@@ -186,19 +229,9 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdHandler := CommandHandler{
|
err = runCommand(os.Args[1], fs.Args()[1:], config, info, cmdHandler)
|
||||||
commands: make(map[string]func(bool, models.Config, pkg.Info, *spinner.Spinner, *models.CustomSpinnerWriter, []string) error),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdHandler.RegisterCmd("deploy", commands.DeployCommand)
|
|
||||||
cmdHandler.RegisterCmd("stop", commands.StopCommand)
|
|
||||||
cmdHandler.RegisterCmd("start", commands.StartCommand)
|
|
||||||
cmdHandler.RegisterCmd("delete", commands.DeleteCommand)
|
|
||||||
cmdHandler.RegisterCmd("init", commands.InitCommand)
|
|
||||||
|
|
||||||
err = runCommand(command, args, config, info, cmdHandler, 0)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%v\n", err)
|
fmt.Printf("Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
cmd/flux/models/cmd.go
Normal file
8
cmd/flux/models/cmd.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "github.com/juls0730/flux/pkg"
|
||||||
|
|
||||||
|
type CommandCtx struct {
|
||||||
|
Config Config
|
||||||
|
Info pkg.Info
|
||||||
|
}
|
||||||
@@ -44,11 +44,14 @@ func NewCustomStdout(spinner *CustomSpinnerWriter) *CustomStdout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We have this custom writer because we want to have a spinner at the bottom of the terminal, but we dont want to have
|
||||||
|
// it interfere with the output of the command
|
||||||
func (w *CustomStdout) Write(p []byte) (n int, err error) {
|
func (w *CustomStdout) Write(p []byte) (n int, err error) {
|
||||||
w.lock.Lock()
|
w.lock.Lock()
|
||||||
defer w.lock.Unlock()
|
defer w.lock.Unlock()
|
||||||
|
|
||||||
n, err = os.Stdout.Write([]byte(fmt.Sprintf("\033[2K\r%s", p)))
|
// clear line and carriage return
|
||||||
|
n, err = os.Stdout.Write(fmt.Appendf(nil, "\033[2K\r%s", p))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ func NewDeploymentLock() *DeploymentLock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dt *DeploymentLock) StartDeployment(appName string, ctx context.Context) (context.Context, error) {
|
// This function will lock a deployment based on an app name so that the same app cannot be deployed twice simultaneously
|
||||||
|
func (dt *DeploymentLock) LockDeployment(appName string, ctx context.Context) (context.Context, error) {
|
||||||
dt.mu.Lock()
|
dt.mu.Lock()
|
||||||
defer dt.mu.Unlock()
|
defer dt.mu.Unlock()
|
||||||
|
|
||||||
@@ -61,7 +62,8 @@ func (dt *DeploymentLock) StartDeployment(appName string, ctx context.Context) (
|
|||||||
return ctx, nil
|
return ctx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dt *DeploymentLock) CompleteDeployment(appName string) {
|
// This function will unlock a deployment based on an app name so that the same app can be deployed again (you would call this after a deployment has completed)
|
||||||
|
func (dt *DeploymentLock) UnlockDeployment(appName string) {
|
||||||
dt.mu.Lock()
|
dt.mu.Lock()
|
||||||
defer dt.mu.Unlock()
|
defer dt.mu.Unlock()
|
||||||
|
|
||||||
@@ -114,7 +116,7 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, err := deploymentLock.StartDeployment(projectConfig.Name, r.Context())
|
ctx, err := deploymentLock.LockDeployment(projectConfig.Name, r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// This will happen if the app is already being deployed
|
// This will happen if the app is already being deployed
|
||||||
http.Error(w, err.Error(), http.StatusConflict)
|
http.Error(w, err.Error(), http.StatusConflict)
|
||||||
@@ -123,7 +125,7 @@ func (s *FluxServer) DeployHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
deploymentLock.CompleteDeployment(projectConfig.Name)
|
deploymentLock.UnlockDeployment(projectConfig.Name)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
flusher, ok := w.(http.Flusher)
|
flusher, ok := w.(http.Flusher)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package pkg
|
package pkg
|
||||||
|
|
||||||
const Version = "2bd953d"
|
const Version = "2025.04.13-05"
|
||||||
|
|||||||
Reference in New Issue
Block a user