diff --git a/cmd/cli/commands/delete.go b/cmd/cli/commands/delete.go index 538e574..fff9c87 100644 --- a/cmd/cli/commands/delete.go +++ b/cmd/cli/commands/delete.go @@ -82,7 +82,7 @@ func DeleteCommand(ctx CommandCtx, args []string) error { 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) + 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 diff --git a/cmd/cli/commands/deploy.go b/cmd/cli/commands/deploy.go index e5576fd..d3cd6d8 100644 --- a/cmd/cli/commands/deploy.go +++ b/cmd/cli/commands/deploy.go @@ -6,6 +6,7 @@ import ( "bytes" "compress/gzip" "encoding/json" + "flag" "fmt" "io" "mime/multipart" @@ -175,11 +176,38 @@ func preprocessEnvFile(envFile string, target *[]string) error { 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 { if _, err := os.Stat("flux.json"); err != nil { 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() 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 case "cmd_output": - customWriter.Printf("... %s\n", data.Message) + // suppress the command output if the quiet flag is set + if quiet == nil || !*quiet { + customWriter.Printf("... %s\n", data.Message) + } case "error": loadingSpinner.Stop() return fmt.Errorf("deployment failed: %s", data.Message) diff --git a/cmd/cli/commands/init.go b/cmd/cli/commands/init.go index 197cbdf..caab2e6 100644 --- a/cmd/cli/commands/init.go +++ b/cmd/cli/commands/init.go @@ -13,17 +13,20 @@ import ( ) var initUsage = `Usage: - flux init [project-name] + flux init [flags] [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.` +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 { - if !ctx.Interactive { - return fmt.Errorf("init command can only be run in interactive mode") - } + var projectConfig pkg.ProjectConfig fs := flag.NewFlagSet("init", flag.ExitOnError) fs.Usage = func() { @@ -32,8 +35,10 @@ func InitCommand(ctx CommandCtx, args []string) error { fs.SetOutput(&buf) 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) if err != nil { @@ -43,10 +48,22 @@ func InitCommand(ctx CommandCtx, args []string) error { 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 - if len(args) > 1 { + if len(args) > 0 { response = args[0] } else { fmt.Println("What is the name of your project?") @@ -55,28 +72,47 @@ func InitCommand(ctx CommandCtx, args []string) error { 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://") + 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.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 } - response = strings.Split(response, "/")[0] + if projectPort != nil && *projectPort != 0 { + if *projectPort < 1024 || *projectPort > 65535 { + return fmt.Errorf("project-port must be between 1024 and 65535") + } - projectConfig.Url = response + projectConfig.Port = uint16(*projectPort) + } else { + 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 + } - 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 + projectConfig.Port = uint16(port) + if err != nil || projectConfig.Port < 1024 { + return portErr + } } configBytes, err := json.MarshalIndent(projectConfig, "", " ") diff --git a/cmd/cli/main.go b/cmd/cli/main.go index f855fb6..7d73c78 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -35,9 +35,9 @@ Available Commands: %s Available Flags: - --help, -h: Show this help message + -help, -h: Show this help message -Use "flux --help" for more information about a command. +Use "flux -help" for more information about a command. ` var maxDistance = 3 diff --git a/internal/docker/container.go b/internal/docker/container.go index 9ad3edf..a6024b4 100644 --- a/internal/docker/container.go +++ b/internal/docker/container.go @@ -152,7 +152,7 @@ func (d *DockerClient) GetContainerStatus(containerID DockerID) (*ContainerStatu } 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{}) } diff --git a/internal/handlers/app.go b/internal/handlers/app.go index e19405b..5dccf1d 100644 --- a/internal/handlers/app.go +++ b/internal/handlers/app.go @@ -557,6 +557,11 @@ func (flux *FluxServer) StopApp(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() for _, app := range apps { err := flux.appManager.DeleteApp(app.Id) @@ -582,13 +587,23 @@ func (flux *FluxServer) DeleteDeployHandler(w http.ResponseWriter, r *http.Reque 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) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - flux.proxy.RemoveDeployment(app.Deployment.URL) - w.WriteHeader(http.StatusOK) } diff --git a/internal/handlers/server.go b/internal/handlers/server.go index 2c34a0c..caebee2 100644 --- a/internal/handlers/server.go +++ b/internal/handlers/server.go @@ -62,7 +62,12 @@ func NewServer() *FluxServer { 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() verbosity = -1 } diff --git a/internal/models/container.go b/internal/models/container.go index 22fa4fe..eb44c67 100644 --- a/internal/models/container.go +++ b/internal/models/container.go @@ -176,9 +176,9 @@ func CreateContainer(ctx context.Context, imageName string, friendlyName string, // 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 { // 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 { - 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) @@ -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 { 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 { 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) { containerJSON, err := dockerClient.ContainerInspect(context.Background(), c.ContainerID) 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 } diff --git a/internal/models/deployment.go b/internal/models/deployment.go index 00255f3..1922904 100644 --- a/internal/models/deployment.go +++ b/internal/models/deployment.go @@ -77,7 +77,7 @@ func (d *Deployment) Start(ctx context.Context, dockerClient *docker.DockerClien for _, container := range d.containers { err := dockerClient.StartContainer(ctx, container.ContainerID) 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 == "" { - 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)) @@ -106,7 +106,7 @@ func (d *Deployment) Stop(ctx context.Context, dockerClient *docker.DockerClient for _, container := range d.containers { err := dockerClient.StopContainer(ctx, container.ContainerID) 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 @@ -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 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 { err := dockerClient.StopContainer(ctx, supplementalContainer.ContainerID) 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) 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) if err != nil { 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 if ok { 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 { logger.Errorw("Failed to remove container", zap.Error(err)) } diff --git a/pkg/version.go b/pkg/version.go index 94629dc..427b3b6 100644 --- a/pkg/version.go +++ b/pkg/version.go @@ -1,3 +1,3 @@ package pkg -const Version = "2025.05.04-00" +const Version = "2025.05.06-16" diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..e95ad69 --- /dev/null +++ b/test.sh @@ -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 \ No newline at end of file diff --git a/zqdgr.config.json b/zqdgr.config.json index f321416..03b1042 100644 --- a/zqdgr.config.json +++ b/zqdgr.config.json @@ -7,9 +7,10 @@ "scripts": { "build:daemon": "go build -o fluxd cmd/daemon/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:cli": "go run cmd/flux/main.go" + "run:cli": "go run cmd/cli/main.go", + "test": "./test.sh" }, "pattern": "**/*.go", "excluded_dirs": []