add tests, fix bugs, and make cli usable without interactivity

This commit is contained in:
Zoe
2025-05-06 11:00:56 -05:00
parent 4ab58f6324
commit 5bb696052a
12 changed files with 216 additions and 47 deletions

View File

@@ -82,7 +82,7 @@ func DeleteCommand(ctx CommandCtx, args []string) error {
project, err := util.GetProject("delete", args, ctx.Config) project, err := util.GetProject("delete", args, ctx.Config)
if err != nil { if err != nil {
return fmt.Errorf("\tfailed to get project name: %v.\n\tSee flux delete --help for more information", err) 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 // ask for confirmation if not --no-confirm

View File

@@ -6,6 +6,7 @@ import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"encoding/json" "encoding/json"
"flag"
"fmt" "fmt"
"io" "io"
"mime/multipart" "mime/multipart"
@@ -175,11 +176,38 @@ func preprocessEnvFile(envFile string, target *[]string) error {
return nil return nil
} }
var deployUsage = `Usage:
flux deploy [flags]
Flags:
-help, -h: Show this help message
%s
Flux will deploy or redeploy the app in the current directory.
`
func DeployCommand(ctx CommandCtx, args []string) error { func DeployCommand(ctx CommandCtx, args []string) error {
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")
} }
fs := flag.NewFlagSet("deploy", 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(deployUsage, strings.TrimRight(buf.String(), "\n"))
}
quiet := fs.Bool("q", false, "Don't print the deployment logs")
err := fs.Parse(args)
if err != nil {
return err
}
spinnerWriter := util.NewCustomSpinnerWriter() spinnerWriter := util.NewCustomSpinnerWriter()
loadingSpinner := spinner.New(spinner.CharSets[14], 100*time.Millisecond, spinner.WithWriter(spinnerWriter)) loadingSpinner := spinner.New(spinner.CharSets[14], 100*time.Millisecond, spinner.WithWriter(spinnerWriter))
@@ -353,7 +381,10 @@ func DeployCommand(ctx CommandCtx, args []string) error {
} }
return nil return nil
case "cmd_output": case "cmd_output":
// suppress the command output if the quiet flag is set
if quiet == nil || !*quiet {
customWriter.Printf("... %s\n", data.Message) customWriter.Printf("... %s\n", data.Message)
}
case "error": case "error":
loadingSpinner.Stop() loadingSpinner.Stop()
return fmt.Errorf("deployment failed: %s", data.Message) return fmt.Errorf("deployment failed: %s", data.Message)

View File

@@ -13,17 +13,20 @@ import (
) )
var initUsage = `Usage: var initUsage = `Usage:
flux init [project-name] flux init [flags] [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.` Flags:
-help, -h: Show this help message
%s
Flux will initialize a new project in the current directory or the specified project.
`
func InitCommand(ctx CommandCtx, args []string) error { func InitCommand(ctx CommandCtx, args []string) error {
if !ctx.Interactive { var projectConfig pkg.ProjectConfig
return fmt.Errorf("init command can only be run in interactive mode")
}
fs := flag.NewFlagSet("init", flag.ExitOnError) fs := flag.NewFlagSet("init", flag.ExitOnError)
fs.Usage = func() { fs.Usage = func() {
@@ -32,8 +35,10 @@ func InitCommand(ctx CommandCtx, args []string) error {
fs.SetOutput(&buf) fs.SetOutput(&buf)
fs.PrintDefaults() fs.PrintDefaults()
fmt.Println(initUsage) fmt.Printf(initUsage, strings.TrimRight(buf.String(), "\n"))
} }
hostUrl := fs.String("host-url", "", "The URL of the host")
projectPort := fs.Uint("project-port", 0, "The port of the host")
err := fs.Parse(args) err := fs.Parse(args)
if err != nil { if err != nil {
@@ -43,10 +48,22 @@ func InitCommand(ctx CommandCtx, args []string) error {
args = fs.Args() args = fs.Args()
var projectConfig pkg.ProjectConfig if !ctx.Interactive {
if hostUrl == nil || *hostUrl == "" {
return fmt.Errorf("host-url is required when not in interactive mode")
}
if projectPort == nil || *projectPort == 0 {
return fmt.Errorf("project-port is required when not in interactive mode")
}
if len(args) < 1 {
return fmt.Errorf("project-name is required when not in interactive mode")
}
}
var response string var response string
if len(args) > 1 { if len(args) > 0 {
response = args[0] response = args[0]
} else { } else {
fmt.Println("What is the name of your project?") fmt.Println("What is the name of your project?")
@@ -55,6 +72,16 @@ func InitCommand(ctx CommandCtx, args []string) error {
projectConfig.Name = response projectConfig.Name = response
if hostUrl != nil && *hostUrl != "" {
if strings.HasPrefix(*hostUrl, "http") {
*hostUrl = strings.TrimPrefix(*hostUrl, "http://")
*hostUrl = strings.TrimPrefix(*hostUrl, "https://")
}
*hostUrl = strings.Split(*hostUrl, "/")[0]
projectConfig.Url = *hostUrl
} else {
fmt.Println("What URL should your project listen to?") fmt.Println("What URL should your project listen to?")
fmt.Scanln(&response) fmt.Scanln(&response)
if strings.HasPrefix(response, "http") { if strings.HasPrefix(response, "http") {
@@ -65,7 +92,15 @@ func InitCommand(ctx CommandCtx, args []string) error {
response = strings.Split(response, "/")[0] response = strings.Split(response, "/")[0]
projectConfig.Url = response projectConfig.Url = response
}
if projectPort != nil && *projectPort != 0 {
if *projectPort < 1024 || *projectPort > 65535 {
return fmt.Errorf("project-port must be between 1024 and 65535")
}
projectConfig.Port = uint16(*projectPort)
} else {
fmt.Println("What port does your project listen to?") fmt.Println("What port does your project listen to?")
fmt.Scanln(&response) fmt.Scanln(&response)
port, err := strconv.ParseUint(response, 10, 16) port, err := strconv.ParseUint(response, 10, 16)
@@ -78,6 +113,7 @@ func InitCommand(ctx CommandCtx, args []string) error {
if err != nil || projectConfig.Port < 1024 { if err != nil || projectConfig.Port < 1024 {
return portErr return portErr
} }
}
configBytes, err := json.MarshalIndent(projectConfig, "", " ") configBytes, err := json.MarshalIndent(projectConfig, "", " ")
if err != nil { if err != nil {

View File

@@ -35,9 +35,9 @@ Available Commands:
%s %s
Available Flags: Available Flags:
--help, -h: Show this help message -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

View File

@@ -152,7 +152,7 @@ func (d *DockerClient) GetContainerStatus(containerID DockerID) (*ContainerStatu
} }
func (d *DockerClient) StopContainer(ctx context.Context, containerID DockerID) error { func (d *DockerClient) StopContainer(ctx context.Context, containerID DockerID) error {
d.logger.Debugw("Stopping container", zap.String("container_id", string(containerID[:12]))) d.logger.Debugw("Stopping container", zap.String("container_id", string(containerID)))
return d.client.ContainerStop(ctx, string(containerID), container.StopOptions{}) return d.client.ContainerStop(ctx, string(containerID), container.StopOptions{})
} }

View File

@@ -557,6 +557,11 @@ func (flux *FluxServer) StopApp(w http.ResponseWriter, r *http.Request) {
} }
func (flux *FluxServer) DeleteAllDeploymentsHandler(w http.ResponseWriter, r *http.Request) { func (flux *FluxServer) DeleteAllDeploymentsHandler(w http.ResponseWriter, r *http.Request) {
if flux.config.DisableDeleteAll {
http.Error(w, "Delete all deployments is disabled", http.StatusForbidden)
return
}
apps := flux.appManager.GetAllApps() apps := flux.appManager.GetAllApps()
for _, app := range apps { for _, app := range apps {
err := flux.appManager.DeleteApp(app.Id) err := flux.appManager.DeleteApp(app.Id)
@@ -582,13 +587,23 @@ func (flux *FluxServer) DeleteDeployHandler(w http.ResponseWriter, r *http.Reque
return return
} }
status, err := app.Deployment.Status(r.Context(), flux.docker, flux.logger)
if err != nil {
flux.logger.Errorw("Failed to get deployment status", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if status != "stopped" {
app.Deployment.Stop(r.Context(), flux.docker)
flux.proxy.RemoveDeployment(app.Deployment.URL)
}
err = flux.appManager.DeleteApp(id) err = flux.appManager.DeleteApp(id)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
flux.proxy.RemoveDeployment(app.Deployment.URL)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }

View File

@@ -62,7 +62,12 @@ func NewServer() *FluxServer {
config := zap.NewProductionConfig() config := zap.NewProductionConfig()
if os.Getenv("DEBUG") == "true" { debug, err := strconv.ParseBool(os.Getenv("DEBUG"))
if err != nil {
debug = false
}
if debug {
config = zap.NewDevelopmentConfig() config = zap.NewDevelopmentConfig()
verbosity = -1 verbosity = -1
} }

View File

@@ -176,9 +176,9 @@ func CreateContainer(ctx context.Context, imageName string, friendlyName string,
// Updates Container in place // Updates Container in place
func (c *Container) Upgrade(ctx context.Context, imageName string, environment []string, dockerClient *docker.DockerClient, db *sql.DB, logger *zap.SugaredLogger) error { func (c *Container) Upgrade(ctx context.Context, imageName string, environment []string, dockerClient *docker.DockerClient, db *sql.DB, logger *zap.SugaredLogger) error {
// Create new container with new image // Create new container with new image
logger.Debugw("Upgrading container", zap.String("container_id", string(c.ContainerID[:12]))) logger.Debugw("Upgrading container", zap.String("container_id", string(c.ContainerID)))
if c.Volumes == nil { if c.Volumes == nil {
return fmt.Errorf("no volumes found for container %s", c.ContainerID[:12]) return fmt.Errorf("no volumes found for container %s", c.ContainerID)
} }
containerJSON, err := dockerClient.ContainerInspect(context.Background(), c.ContainerID) containerJSON, err := dockerClient.ContainerInspect(context.Background(), c.ContainerID)
@@ -269,7 +269,7 @@ func (c *Container) Remove(ctx context.Context, dockerClient *docker.DockerClien
func (c *Container) Start(ctx context.Context, initial bool, db *sql.DB, dockerClient *docker.DockerClient, logger *zap.SugaredLogger) error { func (c *Container) Start(ctx context.Context, initial bool, db *sql.DB, dockerClient *docker.DockerClient, logger *zap.SugaredLogger) error {
logger.Debugf("Starting container %+v", c) logger.Debugf("Starting container %+v", c)
logger.Info("Starting container", zap.String("container_id", string(c.ContainerID)[:12])) logger.Infow("Starting container", zap.String("container_id", string(c.ContainerID)))
if !initial && c.Head { if !initial && c.Head {
logger.Debug("Starting and repairing head container") logger.Debug("Starting and repairing head container")
@@ -330,7 +330,7 @@ func (c *Container) Wait(ctx context.Context, port uint16, dockerClient *docker.
func (c *Container) GetIp(dockerClient *docker.DockerClient, logger *zap.SugaredLogger) (string, error) { func (c *Container) GetIp(dockerClient *docker.DockerClient, logger *zap.SugaredLogger) (string, error) {
containerJSON, err := dockerClient.ContainerInspect(context.Background(), c.ContainerID) containerJSON, err := dockerClient.ContainerInspect(context.Background(), c.ContainerID)
if err != nil { if err != nil {
logger.Errorw("Failed to inspect container", zap.Error(err), zap.String("container_id", string(c.ContainerID[:12]))) logger.Errorw("Failed to inspect container", zap.Error(err), zap.String("container_id", string(c.ContainerID)))
return "", err return "", err
} }

View File

@@ -77,7 +77,7 @@ func (d *Deployment) Start(ctx context.Context, dockerClient *docker.DockerClien
for _, container := range d.containers { for _, container := range d.containers {
err := dockerClient.StartContainer(ctx, container.ContainerID) err := dockerClient.StartContainer(ctx, container.ContainerID)
if err != nil { if err != nil {
return fmt.Errorf("failed to start container (%s): %v", container.ContainerID[:12], err) return fmt.Errorf("failed to start container (%s): %v", container.ContainerID, err)
} }
} }
@@ -91,7 +91,7 @@ func (d *Deployment) GetInternalUrl(dockerClient *docker.DockerClient) (*url.URL
} }
if containerJSON.NetworkSettings.IPAddress == "" { if containerJSON.NetworkSettings.IPAddress == "" {
return nil, fmt.Errorf("no IP address found for container %s", d.Head().ContainerID[:12]) return nil, fmt.Errorf("no IP address found for container %s", d.Head().ContainerID)
} }
containerUrl, err := url.Parse(fmt.Sprintf("http://%s:%d", containerJSON.NetworkSettings.IPAddress, d.Port)) containerUrl, err := url.Parse(fmt.Sprintf("http://%s:%d", containerJSON.NetworkSettings.IPAddress, d.Port))
@@ -106,7 +106,7 @@ func (d *Deployment) Stop(ctx context.Context, dockerClient *docker.DockerClient
for _, container := range d.containers { for _, container := range d.containers {
err := dockerClient.StopContainer(ctx, container.ContainerID) err := dockerClient.StopContainer(ctx, container.ContainerID)
if err != nil { if err != nil {
return fmt.Errorf("failed to stop container (%s): %v", container.ContainerID[:12], err) return fmt.Errorf("failed to stop container (%s): %v", container.ContainerID, err)
} }
} }
return nil return nil
@@ -141,7 +141,7 @@ func (deployment *Deployment) Status(ctx context.Context, dockerClient *docker.D
// if the head is running, but the supplemental container is stopped, return "failed" // if the head is running, but the supplemental container is stopped, return "failed"
if headStatus.Status == "running" && containerStatus.Status != "running" { if headStatus.Status == "running" && containerStatus.Status != "running" {
logger.Debugw("Supplemental container is not running but head is, returning to failed state", zap.String("container_id", string(container.ContainerID[:12]))) logger.Debugw("Supplemental container is not running but head is, returning to failed state", zap.String("container_id", string(container.ContainerID)))
for _, supplementalContainer := range deployment.containers { for _, supplementalContainer := range deployment.containers {
err := dockerClient.StopContainer(ctx, supplementalContainer.ContainerID) err := dockerClient.StopContainer(ctx, supplementalContainer.ContainerID)
if err != nil { if err != nil {
@@ -183,7 +183,7 @@ func (deployment *Deployment) Upgrade(ctx context.Context, projectConfig *pkg.Pr
db.Exec("DELETE FROM containers WHERE id = ?", oldHeadContainer.ID) db.Exec("DELETE FROM containers WHERE id = ?", oldHeadContainer.ID)
newHeadContainer := deployment.Head() newHeadContainer := deployment.Head()
logger.Debugw("Starting container", zap.String("container_id", string(newHeadContainer.ContainerID[:12]))) logger.Debugw("Starting container", zap.String("container_id", string(newHeadContainer.ContainerID)))
err = newHeadContainer.Start(ctx, true, db, dockerClient, logger) err = newHeadContainer.Start(ctx, true, db, dockerClient, logger)
if err != nil { if err != nil {
logger.Errorw("Failed to start container", zap.Error(err)) logger.Errorw("Failed to start container", zap.Error(err))
@@ -221,7 +221,13 @@ func (deployment *Deployment) Upgrade(ctx context.Context, projectConfig *pkg.Pr
// gracefully shutdown the old proxy, or if it doesnt exist, just remove the containers // gracefully shutdown the old proxy, or if it doesnt exist, just remove the containers
if ok { if ok {
go oldProxy.GracefulShutdown(func() { go oldProxy.GracefulShutdown(func() {
err := dockerClient.DeleteDockerContainer(context.Background(), oldHeadContainer.ContainerID) err := dockerClient.StopContainer(context.Background(), oldHeadContainer.ContainerID)
if err != nil {
logger.Errorw("Failed to stop container", zap.Error(err))
return
}
err = dockerClient.DeleteDockerContainer(context.Background(), oldHeadContainer.ContainerID)
if err != nil { if err != nil {
logger.Errorw("Failed to remove container", zap.Error(err)) logger.Errorw("Failed to remove container", zap.Error(err))
} }

View File

@@ -1,3 +1,3 @@
package pkg package pkg
const Version = "2025.05.04-00" const Version = "2025.05.06-16"

75
test.sh Executable file
View File

@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# Basic test for flux
set -ex
Flux_Database_Dir=$(mktemp -d)c
zqdgr build:all
tmux new-session -d -s daemon
# start daemon
tmux send-keys -t daemon "export FLUXD_ROOT_DIR=$Flux_Database_Dir" C-m
tmux send-keys -t daemon "DEBUG=true zqdgr run:daemon" C-m
# test daemon with the cli
tmux split-window -h
export FLUX_CLI_PATH=$PWD/flux
tmux send-keys -t daemon:0.1 "cd \$(mktemp -d)" C-m
tmux send-keys -t daemon:0.1 "cat << EOF > test.sh
#!/usr/bin/env bash
set -xe
# wait for the daemon to initialize
sleep 2
go mod init testApp
$FLUX_CLI_PATH init --host-url testApp --project-port 8080 testApp
cat << ELOF > main.go
package main
import (
\"fmt\"
\"net/http\"
)
func main() {
http.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, \"Hello World\\n\")
})
http.ListenAndServe(\":8080\", nil)
}
ELOF
$FLUX_CLI_PATH deploy -q
curl -H \"Host: testApp\" localhost:7465
$FLUX_CLI_PATH stop
curl -H \"Host: testApp\" localhost:7465
$FLUX_CLI_PATH start
curl -H \"Host: testApp\" localhost:7465
sed -i 's/Hello World/Hello World 2/' main.go
$FLUX_CLI_PATH deploy -q
curl -H \"Host: testApp\" localhost:7465
$FLUX_CLI_PATH delete --no-confirm
EOF
"
tmux send-keys -t daemon:0.1 "chmod +x test.sh" C-m
tmux send-keys -t daemon:0.1 "./test.sh" C-m
tmux attach-session -d

View File

@@ -7,9 +7,10 @@
"scripts": { "scripts": {
"build:daemon": "go build -o fluxd cmd/daemon/main.go", "build:daemon": "go build -o fluxd cmd/daemon/main.go",
"build:cli": "go build -o flux cmd/cli/main.go", "build:cli": "go build -o flux cmd/cli/main.go",
"build:all": "go build -o fluxd cmd/daemon/main.go && go build -o flux cmd/cli/main.go", "build:all": "zqdgr build:daemon && zqdgr build:cli",
"run:daemon": "go run cmd/daemon/main.go", "run:daemon": "go run cmd/daemon/main.go",
"run:cli": "go run cmd/flux/main.go" "run:cli": "go run cmd/cli/main.go",
"test": "./test.sh"
}, },
"pattern": "**/*.go", "pattern": "**/*.go",
"excluded_dirs": [] "excluded_dirs": []