diff options
Diffstat (limited to 'cmd/deploy')
| -rw-r--r-- | cmd/deploy/env/env.go | 17 | ||||
| -rw-r--r-- | cmd/deploy/env/list.go | 69 | ||||
| -rw-r--r-- | cmd/deploy/env/set.go | 132 | ||||
| -rw-r--r-- | cmd/deploy/env/unset.go | 92 | ||||
| -rw-r--r-- | cmd/deploy/host/host.go | 18 | ||||
| -rw-r--r-- | cmd/deploy/host/init.go | 137 | ||||
| -rw-r--r-- | cmd/deploy/host/ssh.go | 45 | ||||
| -rw-r--r-- | cmd/deploy/host/status.go | 108 | ||||
| -rw-r--r-- | cmd/deploy/host/update.go | 93 | ||||
| -rw-r--r-- | cmd/deploy/list.go | 50 | ||||
| -rw-r--r-- | cmd/deploy/logs.go | 75 | ||||
| -rw-r--r-- | cmd/deploy/main.go | 65 | ||||
| -rw-r--r-- | cmd/deploy/remove.go | 83 | ||||
| -rw-r--r-- | cmd/deploy/restart.go | 57 | ||||
| -rw-r--r-- | cmd/deploy/root.go | 377 | ||||
| -rw-r--r-- | cmd/deploy/status.go | 60 | ||||
| -rw-r--r-- | cmd/deploy/templates/webui.html | 440 | ||||
| -rw-r--r-- | cmd/deploy/ui.go | 199 | ||||
| -rw-r--r-- | cmd/deploy/version.go | 17 |
19 files changed, 0 insertions, 2134 deletions
diff --git a/cmd/deploy/env/env.go b/cmd/deploy/env/env.go deleted file mode 100644 index 489353a..0000000 --- a/cmd/deploy/env/env.go +++ /dev/null | |||
| @@ -1,17 +0,0 @@ | |||
| 1 | package env | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "github.com/spf13/cobra" | ||
| 5 | ) | ||
| 6 | |||
| 7 | var Cmd = &cobra.Command{ | ||
| 8 | Use: "env", | ||
| 9 | Short: "Manage environment variables", | ||
| 10 | Long: "Manage environment variables for deployed applications", | ||
| 11 | } | ||
| 12 | |||
| 13 | func init() { | ||
| 14 | Cmd.AddCommand(listCmd) | ||
| 15 | Cmd.AddCommand(setCmd) | ||
| 16 | Cmd.AddCommand(unsetCmd) | ||
| 17 | } | ||
diff --git a/cmd/deploy/env/list.go b/cmd/deploy/env/list.go deleted file mode 100644 index af92171..0000000 --- a/cmd/deploy/env/list.go +++ /dev/null | |||
| @@ -1,69 +0,0 @@ | |||
| 1 | package env | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | "strings" | ||
| 6 | |||
| 7 | "github.com/bdw/deploy/internal/state" | ||
| 8 | "github.com/spf13/cobra" | ||
| 9 | ) | ||
| 10 | |||
| 11 | var listCmd = &cobra.Command{ | ||
| 12 | Use: "list <app>", | ||
| 13 | Short: "List environment variables for an app", | ||
| 14 | Args: cobra.ExactArgs(1), | ||
| 15 | RunE: runList, | ||
| 16 | } | ||
| 17 | |||
| 18 | func runList(cmd *cobra.Command, args []string) error { | ||
| 19 | name := args[0] | ||
| 20 | |||
| 21 | st, err := state.Load() | ||
| 22 | if err != nil { | ||
| 23 | return fmt.Errorf("error loading state: %w", err) | ||
| 24 | } | ||
| 25 | |||
| 26 | host, _ := cmd.Flags().GetString("host") | ||
| 27 | if host == "" { | ||
| 28 | host = st.GetDefaultHost() | ||
| 29 | } | ||
| 30 | |||
| 31 | if host == "" { | ||
| 32 | return fmt.Errorf("--host is required") | ||
| 33 | } | ||
| 34 | |||
| 35 | app, err := st.GetApp(host, name) | ||
| 36 | if err != nil { | ||
| 37 | return err | ||
| 38 | } | ||
| 39 | |||
| 40 | if app.Type != "app" { | ||
| 41 | return fmt.Errorf("env is only available for apps, not static sites") | ||
| 42 | } | ||
| 43 | |||
| 44 | fmt.Printf("Environment variables for %s:\n\n", name) | ||
| 45 | if len(app.Env) == 0 { | ||
| 46 | fmt.Println(" (none)") | ||
| 47 | } else { | ||
| 48 | for k, v := range app.Env { | ||
| 49 | display := v | ||
| 50 | if isSensitive(k) { | ||
| 51 | display = "***" | ||
| 52 | } | ||
| 53 | fmt.Printf(" %s=%s\n", k, display) | ||
| 54 | } | ||
| 55 | } | ||
| 56 | |||
| 57 | return nil | ||
| 58 | } | ||
| 59 | |||
| 60 | func isSensitive(key string) bool { | ||
| 61 | key = strings.ToLower(key) | ||
| 62 | sensitiveWords := []string{"key", "secret", "password", "token", "api"} | ||
| 63 | for _, word := range sensitiveWords { | ||
| 64 | if strings.Contains(key, word) { | ||
| 65 | return true | ||
| 66 | } | ||
| 67 | } | ||
| 68 | return false | ||
| 69 | } | ||
diff --git a/cmd/deploy/env/set.go b/cmd/deploy/env/set.go deleted file mode 100644 index 35d77ff..0000000 --- a/cmd/deploy/env/set.go +++ /dev/null | |||
| @@ -1,132 +0,0 @@ | |||
| 1 | package env | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "bufio" | ||
| 5 | "fmt" | ||
| 6 | "os" | ||
| 7 | "strings" | ||
| 8 | |||
| 9 | "github.com/bdw/deploy/internal/ssh" | ||
| 10 | "github.com/bdw/deploy/internal/state" | ||
| 11 | "github.com/spf13/cobra" | ||
| 12 | ) | ||
| 13 | |||
| 14 | var setCmd = &cobra.Command{ | ||
| 15 | Use: "set <app> KEY=VALUE...", | ||
| 16 | Short: "Set environment variable(s)", | ||
| 17 | Long: "Set one or more environment variables for an app. Variables are specified as KEY=VALUE pairs.", | ||
| 18 | Args: cobra.MinimumNArgs(2), | ||
| 19 | RunE: runSet, | ||
| 20 | } | ||
| 21 | |||
| 22 | func init() { | ||
| 23 | setCmd.Flags().StringP("file", "f", "", "Load environment from file") | ||
| 24 | } | ||
| 25 | |||
| 26 | func runSet(cmd *cobra.Command, args []string) error { | ||
| 27 | name := args[0] | ||
| 28 | envVars := args[1:] | ||
| 29 | |||
| 30 | st, err := state.Load() | ||
| 31 | if err != nil { | ||
| 32 | return fmt.Errorf("error loading state: %w", err) | ||
| 33 | } | ||
| 34 | |||
| 35 | host, _ := cmd.Flags().GetString("host") | ||
| 36 | if host == "" { | ||
| 37 | host = st.GetDefaultHost() | ||
| 38 | } | ||
| 39 | |||
| 40 | if host == "" { | ||
| 41 | return fmt.Errorf("--host is required") | ||
| 42 | } | ||
| 43 | |||
| 44 | app, err := st.GetApp(host, name) | ||
| 45 | if err != nil { | ||
| 46 | return err | ||
| 47 | } | ||
| 48 | |||
| 49 | if app.Type != "app" { | ||
| 50 | return fmt.Errorf("env is only available for apps, not static sites") | ||
| 51 | } | ||
| 52 | |||
| 53 | if app.Env == nil { | ||
| 54 | app.Env = make(map[string]string) | ||
| 55 | } | ||
| 56 | |||
| 57 | // Set variables from args | ||
| 58 | for _, e := range envVars { | ||
| 59 | parts := strings.SplitN(e, "=", 2) | ||
| 60 | if len(parts) == 2 { | ||
| 61 | app.Env[parts[0]] = parts[1] | ||
| 62 | fmt.Printf("Set %s\n", parts[0]) | ||
| 63 | } else { | ||
| 64 | return fmt.Errorf("invalid format: %s (expected KEY=VALUE)", e) | ||
| 65 | } | ||
| 66 | } | ||
| 67 | |||
| 68 | // Set variables from file if provided | ||
| 69 | envFile, _ := cmd.Flags().GetString("file") | ||
| 70 | if envFile != "" { | ||
| 71 | fileEnv, err := parseEnvFile(envFile) | ||
| 72 | if err != nil { | ||
| 73 | return fmt.Errorf("error reading env file: %w", err) | ||
| 74 | } | ||
| 75 | for k, v := range fileEnv { | ||
| 76 | app.Env[k] = v | ||
| 77 | fmt.Printf("Set %s\n", k) | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | if err := st.Save(); err != nil { | ||
| 82 | return fmt.Errorf("error saving state: %w", err) | ||
| 83 | } | ||
| 84 | |||
| 85 | client, err := ssh.Connect(host) | ||
| 86 | if err != nil { | ||
| 87 | return fmt.Errorf("error connecting to VPS: %w", err) | ||
| 88 | } | ||
| 89 | defer client.Close() | ||
| 90 | |||
| 91 | fmt.Println("-> Updating environment file on VPS...") | ||
| 92 | envFilePath := fmt.Sprintf("/etc/deploy/env/%s.env", name) | ||
| 93 | envContent := "" | ||
| 94 | for k, v := range app.Env { | ||
| 95 | envContent += fmt.Sprintf("%s=%s\n", k, v) | ||
| 96 | } | ||
| 97 | if err := client.WriteSudoFile(envFilePath, envContent); err != nil { | ||
| 98 | return fmt.Errorf("error updating env file: %w", err) | ||
| 99 | } | ||
| 100 | |||
| 101 | fmt.Println("-> Restarting service...") | ||
| 102 | if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil { | ||
| 103 | return fmt.Errorf("error restarting service: %w", err) | ||
| 104 | } | ||
| 105 | |||
| 106 | fmt.Println("Environment variables updated successfully") | ||
| 107 | return nil | ||
| 108 | } | ||
| 109 | |||
| 110 | func parseEnvFile(path string) (map[string]string, error) { | ||
| 111 | file, err := os.Open(path) | ||
| 112 | if err != nil { | ||
| 113 | return nil, err | ||
| 114 | } | ||
| 115 | defer file.Close() | ||
| 116 | |||
| 117 | env := make(map[string]string) | ||
| 118 | scanner := bufio.NewScanner(file) | ||
| 119 | for scanner.Scan() { | ||
| 120 | line := strings.TrimSpace(scanner.Text()) | ||
| 121 | if line == "" || strings.HasPrefix(line, "#") { | ||
| 122 | continue | ||
| 123 | } | ||
| 124 | |||
| 125 | parts := strings.SplitN(line, "=", 2) | ||
| 126 | if len(parts) == 2 { | ||
| 127 | env[parts[0]] = parts[1] | ||
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | return env, scanner.Err() | ||
| 132 | } | ||
diff --git a/cmd/deploy/env/unset.go b/cmd/deploy/env/unset.go deleted file mode 100644 index 65a8986..0000000 --- a/cmd/deploy/env/unset.go +++ /dev/null | |||
| @@ -1,92 +0,0 @@ | |||
| 1 | package env | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | |||
| 6 | "github.com/bdw/deploy/internal/ssh" | ||
| 7 | "github.com/bdw/deploy/internal/state" | ||
| 8 | "github.com/spf13/cobra" | ||
| 9 | ) | ||
| 10 | |||
| 11 | var unsetCmd = &cobra.Command{ | ||
| 12 | Use: "unset <app> KEY...", | ||
| 13 | Short: "Unset environment variable(s)", | ||
| 14 | Long: "Remove one or more environment variables from an app.", | ||
| 15 | Args: cobra.MinimumNArgs(2), | ||
| 16 | RunE: runUnset, | ||
| 17 | } | ||
| 18 | |||
| 19 | func runUnset(cmd *cobra.Command, args []string) error { | ||
| 20 | name := args[0] | ||
| 21 | keys := args[1:] | ||
| 22 | |||
| 23 | st, err := state.Load() | ||
| 24 | if err != nil { | ||
| 25 | return fmt.Errorf("error loading state: %w", err) | ||
| 26 | } | ||
| 27 | |||
| 28 | host, _ := cmd.Flags().GetString("host") | ||
| 29 | if host == "" { | ||
| 30 | host = st.GetDefaultHost() | ||
| 31 | } | ||
| 32 | |||
| 33 | if host == "" { | ||
| 34 | return fmt.Errorf("--host is required") | ||
| 35 | } | ||
| 36 | |||
| 37 | app, err := st.GetApp(host, name) | ||
| 38 | if err != nil { | ||
| 39 | return err | ||
| 40 | } | ||
| 41 | |||
| 42 | if app.Type != "app" { | ||
| 43 | return fmt.Errorf("env is only available for apps, not static sites") | ||
| 44 | } | ||
| 45 | |||
| 46 | if app.Env == nil { | ||
| 47 | return fmt.Errorf("no environment variables set") | ||
| 48 | } | ||
| 49 | |||
| 50 | changed := false | ||
| 51 | for _, key := range keys { | ||
| 52 | if _, exists := app.Env[key]; exists { | ||
| 53 | delete(app.Env, key) | ||
| 54 | changed = true | ||
| 55 | fmt.Printf("Unset %s\n", key) | ||
| 56 | } else { | ||
| 57 | fmt.Printf("Warning: %s not found\n", key) | ||
| 58 | } | ||
| 59 | } | ||
| 60 | |||
| 61 | if !changed { | ||
| 62 | return nil | ||
| 63 | } | ||
| 64 | |||
| 65 | if err := st.Save(); err != nil { | ||
| 66 | return fmt.Errorf("error saving state: %w", err) | ||
| 67 | } | ||
| 68 | |||
| 69 | client, err := ssh.Connect(host) | ||
| 70 | if err != nil { | ||
| 71 | return fmt.Errorf("error connecting to VPS: %w", err) | ||
| 72 | } | ||
| 73 | defer client.Close() | ||
| 74 | |||
| 75 | fmt.Println("-> Updating environment file on VPS...") | ||
| 76 | envFilePath := fmt.Sprintf("/etc/deploy/env/%s.env", name) | ||
| 77 | envContent := "" | ||
| 78 | for k, v := range app.Env { | ||
| 79 | envContent += fmt.Sprintf("%s=%s\n", k, v) | ||
| 80 | } | ||
| 81 | if err := client.WriteSudoFile(envFilePath, envContent); err != nil { | ||
| 82 | return fmt.Errorf("error updating env file: %w", err) | ||
| 83 | } | ||
| 84 | |||
| 85 | fmt.Println("-> Restarting service...") | ||
| 86 | if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil { | ||
| 87 | return fmt.Errorf("error restarting service: %w", err) | ||
| 88 | } | ||
| 89 | |||
| 90 | fmt.Println("Environment variables updated successfully") | ||
| 91 | return nil | ||
| 92 | } | ||
diff --git a/cmd/deploy/host/host.go b/cmd/deploy/host/host.go deleted file mode 100644 index 603a946..0000000 --- a/cmd/deploy/host/host.go +++ /dev/null | |||
| @@ -1,18 +0,0 @@ | |||
| 1 | package host | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "github.com/spf13/cobra" | ||
| 5 | ) | ||
| 6 | |||
| 7 | var Cmd = &cobra.Command{ | ||
| 8 | Use: "host", | ||
| 9 | Short: "Manage VPS host", | ||
| 10 | Long: "Commands for managing and monitoring the VPS host", | ||
| 11 | } | ||
| 12 | |||
| 13 | func init() { | ||
| 14 | Cmd.AddCommand(initCmd) | ||
| 15 | Cmd.AddCommand(statusCmd) | ||
| 16 | Cmd.AddCommand(updateCmd) | ||
| 17 | Cmd.AddCommand(sshCmd) | ||
| 18 | } | ||
diff --git a/cmd/deploy/host/init.go b/cmd/deploy/host/init.go deleted file mode 100644 index 984e5d3..0000000 --- a/cmd/deploy/host/init.go +++ /dev/null | |||
| @@ -1,137 +0,0 @@ | |||
| 1 | package host | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | "strings" | ||
| 6 | |||
| 7 | "github.com/bdw/deploy/internal/ssh" | ||
| 8 | "github.com/bdw/deploy/internal/state" | ||
| 9 | "github.com/spf13/cobra" | ||
| 10 | ) | ||
| 11 | |||
| 12 | var initCmd = &cobra.Command{ | ||
| 13 | Use: "init", | ||
| 14 | Short: "Initialize VPS (one-time setup)", | ||
| 15 | Long: "Set up a fresh VPS with Caddy for automatic HTTPS and required directories", | ||
| 16 | RunE: runInit, | ||
| 17 | } | ||
| 18 | |||
| 19 | func runInit(cmd *cobra.Command, args []string) error { | ||
| 20 | st, err := state.Load() | ||
| 21 | if err != nil { | ||
| 22 | return fmt.Errorf("error loading state: %w", err) | ||
| 23 | } | ||
| 24 | |||
| 25 | host, _ := cmd.Flags().GetString("host") | ||
| 26 | if host == "" { | ||
| 27 | host = st.GetDefaultHost() | ||
| 28 | } | ||
| 29 | |||
| 30 | if host == "" { | ||
| 31 | return fmt.Errorf("--host is required") | ||
| 32 | } | ||
| 33 | |||
| 34 | fmt.Printf("Initializing VPS: %s\n", host) | ||
| 35 | |||
| 36 | client, err := ssh.Connect(host) | ||
| 37 | if err != nil { | ||
| 38 | return fmt.Errorf("error connecting to VPS: %w", err) | ||
| 39 | } | ||
| 40 | defer client.Close() | ||
| 41 | |||
| 42 | fmt.Println("-> Detecting OS...") | ||
| 43 | osRelease, err := client.Run("cat /etc/os-release") | ||
| 44 | if err != nil { | ||
| 45 | return fmt.Errorf("error detecting OS: %w", err) | ||
| 46 | } | ||
| 47 | |||
| 48 | if !strings.Contains(osRelease, "Ubuntu") && !strings.Contains(osRelease, "Debian") { | ||
| 49 | return fmt.Errorf("unsupported OS (only Ubuntu and Debian are supported)") | ||
| 50 | } | ||
| 51 | fmt.Println(" Detected Ubuntu/Debian") | ||
| 52 | |||
| 53 | fmt.Println("-> Checking for Caddy...") | ||
| 54 | _, err = client.Run("which caddy") | ||
| 55 | if err == nil { | ||
| 56 | fmt.Println(" Caddy already installed") | ||
| 57 | } else { | ||
| 58 | fmt.Println(" Installing Caddy...") | ||
| 59 | if err := installCaddy(client); err != nil { | ||
| 60 | return err | ||
| 61 | } | ||
| 62 | fmt.Println(" Caddy installed") | ||
| 63 | } | ||
| 64 | |||
| 65 | fmt.Println("-> Configuring Caddy...") | ||
| 66 | caddyfile := `{ | ||
| 67 | email admin@example.com | ||
| 68 | } | ||
| 69 | |||
| 70 | import /etc/caddy/sites-enabled/* | ||
| 71 | ` | ||
| 72 | if err := client.WriteSudoFile("/etc/caddy/Caddyfile", caddyfile); err != nil { | ||
| 73 | return fmt.Errorf("error creating Caddyfile: %w", err) | ||
| 74 | } | ||
| 75 | fmt.Println(" Caddyfile created") | ||
| 76 | |||
| 77 | fmt.Println("-> Creating directories...") | ||
| 78 | if _, err := client.RunSudo("mkdir -p /etc/deploy/env"); err != nil { | ||
| 79 | return fmt.Errorf("error creating /etc/deploy/env: %w", err) | ||
| 80 | } | ||
| 81 | if _, err := client.RunSudo("mkdir -p /etc/caddy/sites-enabled"); err != nil { | ||
| 82 | return fmt.Errorf("error creating /etc/caddy/sites-enabled: %w", err) | ||
| 83 | } | ||
| 84 | fmt.Println(" Directories created") | ||
| 85 | |||
| 86 | fmt.Println("-> Starting Caddy...") | ||
| 87 | if _, err := client.RunSudo("systemctl enable caddy"); err != nil { | ||
| 88 | return fmt.Errorf("error enabling Caddy: %w", err) | ||
| 89 | } | ||
| 90 | if _, err := client.RunSudo("systemctl restart caddy"); err != nil { | ||
| 91 | return fmt.Errorf("error starting Caddy: %w", err) | ||
| 92 | } | ||
| 93 | fmt.Println(" Caddy started") | ||
| 94 | |||
| 95 | fmt.Println("-> Verifying installation...") | ||
| 96 | output, err := client.RunSudo("systemctl is-active caddy") | ||
| 97 | if err != nil || strings.TrimSpace(output) != "active" { | ||
| 98 | fmt.Println(" Warning: Caddy may not be running properly") | ||
| 99 | } else { | ||
| 100 | fmt.Println(" Caddy is active") | ||
| 101 | } | ||
| 102 | |||
| 103 | st.GetHost(host) | ||
| 104 | if st.GetDefaultHost() == "" { | ||
| 105 | st.SetDefaultHost(host) | ||
| 106 | fmt.Printf(" Set %s as default host\n", host) | ||
| 107 | } | ||
| 108 | if err := st.Save(); err != nil { | ||
| 109 | return fmt.Errorf("error saving state: %w", err) | ||
| 110 | } | ||
| 111 | |||
| 112 | fmt.Println("\nVPS initialized successfully!") | ||
| 113 | fmt.Println("\nNext steps:") | ||
| 114 | fmt.Println(" 1. Deploy a Go app:") | ||
| 115 | fmt.Printf(" deploy --binary ./myapp --domain api.example.com\n") | ||
| 116 | fmt.Println(" 2. Deploy a static site:") | ||
| 117 | fmt.Printf(" deploy --static --dir ./dist --domain example.com\n") | ||
| 118 | return nil | ||
| 119 | } | ||
| 120 | |||
| 121 | func installCaddy(client *ssh.Client) error { | ||
| 122 | commands := []string{ | ||
| 123 | "apt-get update", | ||
| 124 | "apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl", | ||
| 125 | "curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg", | ||
| 126 | "curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list", | ||
| 127 | "apt-get update", | ||
| 128 | "apt-get install -y caddy", | ||
| 129 | } | ||
| 130 | |||
| 131 | for _, cmd := range commands { | ||
| 132 | if _, err := client.RunSudo(cmd); err != nil { | ||
| 133 | return fmt.Errorf("error running: %s: %w", cmd, err) | ||
| 134 | } | ||
| 135 | } | ||
| 136 | return nil | ||
| 137 | } | ||
diff --git a/cmd/deploy/host/ssh.go b/cmd/deploy/host/ssh.go deleted file mode 100644 index a33986f..0000000 --- a/cmd/deploy/host/ssh.go +++ /dev/null | |||
| @@ -1,45 +0,0 @@ | |||
| 1 | package host | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | "os" | ||
| 6 | "os/exec" | ||
| 7 | |||
| 8 | "github.com/bdw/deploy/internal/state" | ||
| 9 | "github.com/spf13/cobra" | ||
| 10 | ) | ||
| 11 | |||
| 12 | var sshCmd = &cobra.Command{ | ||
| 13 | Use: "ssh", | ||
| 14 | Short: "Open interactive SSH session", | ||
| 15 | RunE: runSSH, | ||
| 16 | } | ||
| 17 | |||
| 18 | func runSSH(cmd *cobra.Command, args []string) error { | ||
| 19 | st, err := state.Load() | ||
| 20 | if err != nil { | ||
| 21 | return fmt.Errorf("error loading state: %w", err) | ||
| 22 | } | ||
| 23 | |||
| 24 | host, _ := cmd.Flags().GetString("host") | ||
| 25 | if host == "" { | ||
| 26 | host = st.GetDefaultHost() | ||
| 27 | } | ||
| 28 | |||
| 29 | if host == "" { | ||
| 30 | return fmt.Errorf("--host is required (no default host set)") | ||
| 31 | } | ||
| 32 | |||
| 33 | sshCmd := exec.Command("ssh", host) | ||
| 34 | sshCmd.Stdin = os.Stdin | ||
| 35 | sshCmd.Stdout = os.Stdout | ||
| 36 | sshCmd.Stderr = os.Stderr | ||
| 37 | |||
| 38 | if err := sshCmd.Run(); err != nil { | ||
| 39 | if exitErr, ok := err.(*exec.ExitError); ok { | ||
| 40 | os.Exit(exitErr.ExitCode()) | ||
| 41 | } | ||
| 42 | return err | ||
| 43 | } | ||
| 44 | return nil | ||
| 45 | } | ||
diff --git a/cmd/deploy/host/status.go b/cmd/deploy/host/status.go deleted file mode 100644 index bdd9c31..0000000 --- a/cmd/deploy/host/status.go +++ /dev/null | |||
| @@ -1,108 +0,0 @@ | |||
| 1 | package host | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | |||
| 6 | "github.com/bdw/deploy/internal/ssh" | ||
| 7 | "github.com/bdw/deploy/internal/state" | ||
| 8 | "github.com/spf13/cobra" | ||
| 9 | ) | ||
| 10 | |||
| 11 | var statusCmd = &cobra.Command{ | ||
| 12 | Use: "status", | ||
| 13 | Short: "Show VPS health (uptime, disk, memory)", | ||
| 14 | RunE: runStatus, | ||
| 15 | } | ||
| 16 | |||
| 17 | func runStatus(cmd *cobra.Command, args []string) error { | ||
| 18 | st, err := state.Load() | ||
| 19 | if err != nil { | ||
| 20 | return fmt.Errorf("error loading state: %w", err) | ||
| 21 | } | ||
| 22 | |||
| 23 | host, _ := cmd.Flags().GetString("host") | ||
| 24 | if host == "" { | ||
| 25 | host = st.GetDefaultHost() | ||
| 26 | } | ||
| 27 | |||
| 28 | if host == "" { | ||
| 29 | return fmt.Errorf("--host is required (no default host set)") | ||
| 30 | } | ||
| 31 | |||
| 32 | fmt.Printf("Connecting to %s...\n\n", host) | ||
| 33 | |||
| 34 | client, err := ssh.Connect(host) | ||
| 35 | if err != nil { | ||
| 36 | return fmt.Errorf("error connecting to VPS: %w", err) | ||
| 37 | } | ||
| 38 | defer client.Close() | ||
| 39 | |||
| 40 | fmt.Println("UPTIME") | ||
| 41 | if output, err := client.Run("uptime -p"); err == nil { | ||
| 42 | fmt.Printf(" %s", output) | ||
| 43 | } | ||
| 44 | if output, err := client.Run("uptime -s"); err == nil { | ||
| 45 | fmt.Printf(" Since: %s", output) | ||
| 46 | } | ||
| 47 | fmt.Println() | ||
| 48 | |||
| 49 | fmt.Println("LOAD") | ||
| 50 | if output, err := client.Run("cat /proc/loadavg | awk '{print $1, $2, $3}'"); err == nil { | ||
| 51 | fmt.Printf(" 1m, 5m, 15m: %s", output) | ||
| 52 | } | ||
| 53 | fmt.Println() | ||
| 54 | |||
| 55 | fmt.Println("MEMORY") | ||
| 56 | if output, err := client.Run("free -h | awk 'NR==2 {print \" Used: \" $3 \" / \" $2}'"); err == nil { | ||
| 57 | fmt.Print(output) | ||
| 58 | } | ||
| 59 | if output, err := client.Run("free -h | awk 'NR==2 {printf \" Available: %s\\n\", $7}'"); err == nil { | ||
| 60 | fmt.Print(output) | ||
| 61 | } | ||
| 62 | fmt.Println() | ||
| 63 | |||
| 64 | fmt.Println("DISK") | ||
| 65 | if output, err := client.Run("df -h / | awk 'NR==2 {print \" Used: \" $3 \" / \" $2 \" (\" $5 \")\"}'"); err == nil { | ||
| 66 | fmt.Print(output) | ||
| 67 | } | ||
| 68 | if output, err := client.Run("df -h / | awk 'NR==2 {print \" Available: \" $4}'"); err == nil { | ||
| 69 | fmt.Print(output) | ||
| 70 | } | ||
| 71 | fmt.Println() | ||
| 72 | |||
| 73 | fmt.Println("UPDATES") | ||
| 74 | if output, err := client.Run("[ -f /var/lib/update-notifier/updates-available ] && cat /var/lib/update-notifier/updates-available | head -2 || echo ' (update info not available)'"); err == nil { | ||
| 75 | fmt.Print(output) | ||
| 76 | } | ||
| 77 | fmt.Println() | ||
| 78 | |||
| 79 | fmt.Println("SERVICES") | ||
| 80 | if output, err := client.Run("systemctl is-active caddy 2>/dev/null && echo ' Caddy: active' || echo ' Caddy: inactive'"); err == nil { | ||
| 81 | if output == "active\n" { | ||
| 82 | fmt.Println(" Caddy: active") | ||
| 83 | } else { | ||
| 84 | fmt.Println(" Caddy: inactive") | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | hostState := st.GetHost(host) | ||
| 89 | if hostState != nil && len(hostState.Apps) > 0 { | ||
| 90 | activeCount := 0 | ||
| 91 | for name, app := range hostState.Apps { | ||
| 92 | if app.Type == "app" { | ||
| 93 | if output, err := client.Run(fmt.Sprintf("systemctl is-active %s 2>/dev/null", name)); err == nil && output == "active\n" { | ||
| 94 | activeCount++ | ||
| 95 | } | ||
| 96 | } | ||
| 97 | } | ||
| 98 | appCount := 0 | ||
| 99 | for _, app := range hostState.Apps { | ||
| 100 | if app.Type == "app" { | ||
| 101 | appCount++ | ||
| 102 | } | ||
| 103 | } | ||
| 104 | fmt.Printf(" Deployed apps: %d/%d active\n", activeCount, appCount) | ||
| 105 | } | ||
| 106 | |||
| 107 | return nil | ||
| 108 | } | ||
diff --git a/cmd/deploy/host/update.go b/cmd/deploy/host/update.go deleted file mode 100644 index aa47ed8..0000000 --- a/cmd/deploy/host/update.go +++ /dev/null | |||
| @@ -1,93 +0,0 @@ | |||
| 1 | package host | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "bufio" | ||
| 5 | "fmt" | ||
| 6 | "os" | ||
| 7 | "strings" | ||
| 8 | |||
| 9 | "github.com/bdw/deploy/internal/ssh" | ||
| 10 | "github.com/bdw/deploy/internal/state" | ||
| 11 | "github.com/spf13/cobra" | ||
| 12 | ) | ||
| 13 | |||
| 14 | var updateCmd = &cobra.Command{ | ||
| 15 | Use: "update", | ||
| 16 | Short: "Update VPS packages", | ||
| 17 | Long: "Run apt update && apt upgrade on the VPS", | ||
| 18 | RunE: runUpdate, | ||
| 19 | } | ||
| 20 | |||
| 21 | func init() { | ||
| 22 | updateCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") | ||
| 23 | } | ||
| 24 | |||
| 25 | func runUpdate(cmd *cobra.Command, args []string) error { | ||
| 26 | st, err := state.Load() | ||
| 27 | if err != nil { | ||
| 28 | return fmt.Errorf("error loading state: %w", err) | ||
| 29 | } | ||
| 30 | |||
| 31 | host, _ := cmd.Flags().GetString("host") | ||
| 32 | if host == "" { | ||
| 33 | host = st.GetDefaultHost() | ||
| 34 | } | ||
| 35 | |||
| 36 | if host == "" { | ||
| 37 | return fmt.Errorf("--host is required (no default host set)") | ||
| 38 | } | ||
| 39 | |||
| 40 | yes, _ := cmd.Flags().GetBool("yes") | ||
| 41 | if !yes { | ||
| 42 | fmt.Printf("This will run apt update && apt upgrade on %s\n", host) | ||
| 43 | fmt.Print("Continue? [Y/n]: ") | ||
| 44 | reader := bufio.NewReader(os.Stdin) | ||
| 45 | response, _ := reader.ReadString('\n') | ||
| 46 | response = strings.TrimSpace(response) | ||
| 47 | if response == "n" || response == "N" { | ||
| 48 | fmt.Println("Aborted.") | ||
| 49 | return nil | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 53 | fmt.Printf("Connecting to %s...\n", host) | ||
| 54 | |||
| 55 | client, err := ssh.Connect(host) | ||
| 56 | if err != nil { | ||
| 57 | return fmt.Errorf("error connecting to VPS: %w", err) | ||
| 58 | } | ||
| 59 | defer client.Close() | ||
| 60 | |||
| 61 | fmt.Println("\n-> Running apt update...") | ||
| 62 | if err := client.RunSudoStream("apt update"); err != nil { | ||
| 63 | return fmt.Errorf("error running apt update: %w", err) | ||
| 64 | } | ||
| 65 | |||
| 66 | fmt.Println("\n-> Running apt upgrade...") | ||
| 67 | if err := client.RunSudoStream("DEBIAN_FRONTEND=noninteractive apt upgrade -y"); err != nil { | ||
| 68 | return fmt.Errorf("error running apt upgrade: %w", err) | ||
| 69 | } | ||
| 70 | |||
| 71 | fmt.Println() | ||
| 72 | if output, err := client.Run("[ -f /var/run/reboot-required ] && echo 'yes' || echo 'no'"); err == nil { | ||
| 73 | if strings.TrimSpace(output) == "yes" { | ||
| 74 | fmt.Print("A reboot is required to complete the update. Reboot now? [Y/n]: ") | ||
| 75 | reader := bufio.NewReader(os.Stdin) | ||
| 76 | response, _ := reader.ReadString('\n') | ||
| 77 | response = strings.TrimSpace(response) | ||
| 78 | if response == "" || response == "y" || response == "Y" { | ||
| 79 | fmt.Println("Rebooting...") | ||
| 80 | if _, err := client.RunSudo("reboot"); err != nil { | ||
| 81 | // reboot command often returns an error as connection drops | ||
| 82 | // this is expected behavior | ||
| 83 | } | ||
| 84 | fmt.Println("Reboot initiated. The host will be back online shortly.") | ||
| 85 | return nil | ||
| 86 | } | ||
| 87 | fmt.Println("Skipping reboot. Run 'sudo reboot' manually when ready.") | ||
| 88 | } | ||
| 89 | } | ||
| 90 | |||
| 91 | fmt.Println("Update complete") | ||
| 92 | return nil | ||
| 93 | } | ||
diff --git a/cmd/deploy/list.go b/cmd/deploy/list.go deleted file mode 100644 index ab19a12..0000000 --- a/cmd/deploy/list.go +++ /dev/null | |||
| @@ -1,50 +0,0 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | "os" | ||
| 6 | "text/tabwriter" | ||
| 7 | |||
| 8 | "github.com/bdw/deploy/internal/state" | ||
| 9 | "github.com/spf13/cobra" | ||
| 10 | ) | ||
| 11 | |||
| 12 | var listCmd = &cobra.Command{ | ||
| 13 | Use: "list", | ||
| 14 | Short: "List all deployed apps and sites", | ||
| 15 | RunE: runList, | ||
| 16 | } | ||
| 17 | |||
| 18 | func runList(cmd *cobra.Command, args []string) error { | ||
| 19 | st, err := state.Load() | ||
| 20 | if err != nil { | ||
| 21 | return fmt.Errorf("error loading state: %w", err) | ||
| 22 | } | ||
| 23 | |||
| 24 | host := hostFlag | ||
| 25 | if host == "" { | ||
| 26 | host = st.GetDefaultHost() | ||
| 27 | } | ||
| 28 | |||
| 29 | if host == "" { | ||
| 30 | return fmt.Errorf("--host is required") | ||
| 31 | } | ||
| 32 | |||
| 33 | apps := st.ListApps(host) | ||
| 34 | if len(apps) == 0 { | ||
| 35 | fmt.Printf("No deployments found for %s\n", host) | ||
| 36 | return nil | ||
| 37 | } | ||
| 38 | |||
| 39 | w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) | ||
| 40 | fmt.Fprintln(w, "NAME\tTYPE\tDOMAIN\tPORT") | ||
| 41 | for name, app := range apps { | ||
| 42 | port := "" | ||
| 43 | if app.Type == "app" { | ||
| 44 | port = fmt.Sprintf(":%d", app.Port) | ||
| 45 | } | ||
| 46 | fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, app.Type, app.Domain, port) | ||
| 47 | } | ||
| 48 | w.Flush() | ||
| 49 | return nil | ||
| 50 | } | ||
diff --git a/cmd/deploy/logs.go b/cmd/deploy/logs.go deleted file mode 100644 index 2b016b8..0000000 --- a/cmd/deploy/logs.go +++ /dev/null | |||
| @@ -1,75 +0,0 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | |||
| 6 | "github.com/bdw/deploy/internal/ssh" | ||
| 7 | "github.com/bdw/deploy/internal/state" | ||
| 8 | "github.com/spf13/cobra" | ||
| 9 | ) | ||
| 10 | |||
| 11 | var logsCmd = &cobra.Command{ | ||
| 12 | Use: "logs <app>", | ||
| 13 | Short: "View logs for a deployment", | ||
| 14 | Args: cobra.ExactArgs(1), | ||
| 15 | RunE: runLogs, | ||
| 16 | } | ||
| 17 | |||
| 18 | func init() { | ||
| 19 | logsCmd.Flags().BoolP("follow", "f", false, "Follow logs") | ||
| 20 | logsCmd.Flags().IntP("lines", "n", 50, "Number of lines to show") | ||
| 21 | } | ||
| 22 | |||
| 23 | func runLogs(cmd *cobra.Command, args []string) error { | ||
| 24 | name := args[0] | ||
| 25 | follow, _ := cmd.Flags().GetBool("follow") | ||
| 26 | lines, _ := cmd.Flags().GetInt("lines") | ||
| 27 | |||
| 28 | st, err := state.Load() | ||
| 29 | if err != nil { | ||
| 30 | return fmt.Errorf("error loading state: %w", err) | ||
| 31 | } | ||
| 32 | |||
| 33 | host := hostFlag | ||
| 34 | if host == "" { | ||
| 35 | host = st.GetDefaultHost() | ||
| 36 | } | ||
| 37 | |||
| 38 | if host == "" { | ||
| 39 | return fmt.Errorf("--host is required") | ||
| 40 | } | ||
| 41 | |||
| 42 | app, err := st.GetApp(host, name) | ||
| 43 | if err != nil { | ||
| 44 | return err | ||
| 45 | } | ||
| 46 | |||
| 47 | if app.Type != "app" { | ||
| 48 | return fmt.Errorf("logs are only available for apps, not static sites") | ||
| 49 | } | ||
| 50 | |||
| 51 | client, err := ssh.Connect(host) | ||
| 52 | if err != nil { | ||
| 53 | return fmt.Errorf("error connecting to VPS: %w", err) | ||
| 54 | } | ||
| 55 | defer client.Close() | ||
| 56 | |||
| 57 | journalCmd := fmt.Sprintf("journalctl -u %s -n %d", name, lines) | ||
| 58 | if follow { | ||
| 59 | journalCmd += " -f" | ||
| 60 | } | ||
| 61 | |||
| 62 | if follow { | ||
| 63 | if err := client.RunStream(journalCmd); err != nil { | ||
| 64 | return fmt.Errorf("error fetching logs: %w", err) | ||
| 65 | } | ||
| 66 | } else { | ||
| 67 | output, err := client.Run(journalCmd) | ||
| 68 | if err != nil { | ||
| 69 | return fmt.Errorf("error fetching logs: %w", err) | ||
| 70 | } | ||
| 71 | fmt.Print(output) | ||
| 72 | } | ||
| 73 | |||
| 74 | return nil | ||
| 75 | } | ||
diff --git a/cmd/deploy/main.go b/cmd/deploy/main.go deleted file mode 100644 index ad61523..0000000 --- a/cmd/deploy/main.go +++ /dev/null | |||
| @@ -1,65 +0,0 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "os" | ||
| 5 | |||
| 6 | "github.com/bdw/deploy/cmd/deploy/env" | ||
| 7 | "github.com/bdw/deploy/cmd/deploy/host" | ||
| 8 | "github.com/spf13/cobra" | ||
| 9 | ) | ||
| 10 | |||
| 11 | var ( | ||
| 12 | // Persistent flags | ||
| 13 | hostFlag string | ||
| 14 | |||
| 15 | // Version info (set via ldflags) | ||
| 16 | version = "dev" | ||
| 17 | commit = "none" | ||
| 18 | date = "unknown" | ||
| 19 | ) | ||
| 20 | |||
| 21 | var rootCmd = &cobra.Command{ | ||
| 22 | Use: "deploy", | ||
| 23 | Short: "Deploy Go apps and static sites to a VPS with automatic HTTPS", | ||
| 24 | Long: `deploy - Deploy Go apps and static sites to a VPS with automatic HTTPS | ||
| 25 | |||
| 26 | A CLI tool for deploying applications and static sites to a VPS. | ||
| 27 | Uses Caddy for automatic HTTPS and systemd for service management.`, | ||
| 28 | RunE: runDeploy, | ||
| 29 | SilenceUsage: true, | ||
| 30 | SilenceErrors: true, | ||
| 31 | } | ||
| 32 | |||
| 33 | func init() { | ||
| 34 | // Persistent flags available to all subcommands | ||
| 35 | rootCmd.PersistentFlags().StringVar(&hostFlag, "host", "", "VPS host (SSH config alias or user@host)") | ||
| 36 | |||
| 37 | // Root command (deploy) flags | ||
| 38 | rootCmd.Flags().String("binary", "", "Path to Go binary (for app deployment)") | ||
| 39 | rootCmd.Flags().Bool("static", false, "Deploy as static site") | ||
| 40 | rootCmd.Flags().String("dir", ".", "Directory to deploy (for static sites)") | ||
| 41 | rootCmd.Flags().String("domain", "", "Domain name (required)") | ||
| 42 | rootCmd.Flags().String("name", "", "App name (default: inferred from binary or directory)") | ||
| 43 | rootCmd.Flags().Int("port", 0, "Port override (default: auto-allocate)") | ||
| 44 | rootCmd.Flags().StringArray("env", nil, "Environment variable (KEY=VALUE, can be specified multiple times)") | ||
| 45 | rootCmd.Flags().String("env-file", "", "Path to .env file") | ||
| 46 | rootCmd.Flags().String("args", "", "Arguments to pass to binary") | ||
| 47 | rootCmd.Flags().StringArray("file", nil, "Config file to upload to working directory (can be specified multiple times)") | ||
| 48 | |||
| 49 | // Add subcommands | ||
| 50 | rootCmd.AddCommand(listCmd) | ||
| 51 | rootCmd.AddCommand(logsCmd) | ||
| 52 | rootCmd.AddCommand(statusCmd) | ||
| 53 | rootCmd.AddCommand(restartCmd) | ||
| 54 | rootCmd.AddCommand(removeCmd) | ||
| 55 | rootCmd.AddCommand(env.Cmd) | ||
| 56 | rootCmd.AddCommand(host.Cmd) | ||
| 57 | rootCmd.AddCommand(uiCmd) | ||
| 58 | rootCmd.AddCommand(versionCmd) | ||
| 59 | } | ||
| 60 | |||
| 61 | func main() { | ||
| 62 | if err := rootCmd.Execute(); err != nil { | ||
| 63 | os.Exit(1) | ||
| 64 | } | ||
| 65 | } | ||
diff --git a/cmd/deploy/remove.go b/cmd/deploy/remove.go deleted file mode 100644 index 5a98bf3..0000000 --- a/cmd/deploy/remove.go +++ /dev/null | |||
| @@ -1,83 +0,0 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | |||
| 6 | "github.com/bdw/deploy/internal/ssh" | ||
| 7 | "github.com/bdw/deploy/internal/state" | ||
| 8 | "github.com/spf13/cobra" | ||
| 9 | ) | ||
| 10 | |||
| 11 | var removeCmd = &cobra.Command{ | ||
| 12 | Use: "remove <app>", | ||
| 13 | Aliases: []string{"rm"}, | ||
| 14 | Short: "Remove a deployment", | ||
| 15 | Args: cobra.ExactArgs(1), | ||
| 16 | RunE: runRemove, | ||
| 17 | } | ||
| 18 | |||
| 19 | func runRemove(cmd *cobra.Command, args []string) error { | ||
| 20 | name := args[0] | ||
| 21 | |||
| 22 | st, err := state.Load() | ||
| 23 | if err != nil { | ||
| 24 | return fmt.Errorf("error loading state: %w", err) | ||
| 25 | } | ||
| 26 | |||
| 27 | host := hostFlag | ||
| 28 | if host == "" { | ||
| 29 | host = st.GetDefaultHost() | ||
| 30 | } | ||
| 31 | |||
| 32 | if host == "" { | ||
| 33 | return fmt.Errorf("--host is required") | ||
| 34 | } | ||
| 35 | |||
| 36 | app, err := st.GetApp(host, name) | ||
| 37 | if err != nil { | ||
| 38 | return err | ||
| 39 | } | ||
| 40 | |||
| 41 | fmt.Printf("Removing deployment: %s\n", name) | ||
| 42 | |||
| 43 | client, err := ssh.Connect(host) | ||
| 44 | if err != nil { | ||
| 45 | return fmt.Errorf("error connecting to VPS: %w", err) | ||
| 46 | } | ||
| 47 | defer client.Close() | ||
| 48 | |||
| 49 | if app.Type == "app" { | ||
| 50 | fmt.Println("-> Stopping service...") | ||
| 51 | client.RunSudo(fmt.Sprintf("systemctl stop %s", name)) | ||
| 52 | client.RunSudo(fmt.Sprintf("systemctl disable %s", name)) | ||
| 53 | |||
| 54 | client.RunSudo(fmt.Sprintf("rm -f /etc/systemd/system/%s.service", name)) | ||
| 55 | client.RunSudo("systemctl daemon-reload") | ||
| 56 | |||
| 57 | client.RunSudo(fmt.Sprintf("rm -f /usr/local/bin/%s", name)) | ||
| 58 | client.RunSudo(fmt.Sprintf("rm -rf /var/lib/%s", name)) | ||
| 59 | client.RunSudo(fmt.Sprintf("rm -f /etc/deploy/env/%s.env", name)) | ||
| 60 | client.RunSudo(fmt.Sprintf("userdel %s", name)) | ||
| 61 | } else { | ||
| 62 | fmt.Println("-> Removing files...") | ||
| 63 | client.RunSudo(fmt.Sprintf("rm -rf /var/www/%s", name)) | ||
| 64 | } | ||
| 65 | |||
| 66 | fmt.Println("-> Removing Caddy config...") | ||
| 67 | client.RunSudo(fmt.Sprintf("rm -f /etc/caddy/sites-enabled/%s.caddy", name)) | ||
| 68 | |||
| 69 | fmt.Println("-> Reloading Caddy...") | ||
| 70 | if _, err := client.RunSudo("systemctl reload caddy"); err != nil { | ||
| 71 | fmt.Printf("Warning: Error reloading Caddy: %v\n", err) | ||
| 72 | } | ||
| 73 | |||
| 74 | if err := st.RemoveApp(host, name); err != nil { | ||
| 75 | return fmt.Errorf("error updating state: %w", err) | ||
| 76 | } | ||
| 77 | if err := st.Save(); err != nil { | ||
| 78 | return fmt.Errorf("error saving state: %w", err) | ||
| 79 | } | ||
| 80 | |||
| 81 | fmt.Println("Deployment removed successfully") | ||
| 82 | return nil | ||
| 83 | } | ||
diff --git a/cmd/deploy/restart.go b/cmd/deploy/restart.go deleted file mode 100644 index d1cfa86..0000000 --- a/cmd/deploy/restart.go +++ /dev/null | |||
| @@ -1,57 +0,0 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | |||
| 6 | "github.com/bdw/deploy/internal/ssh" | ||
| 7 | "github.com/bdw/deploy/internal/state" | ||
| 8 | "github.com/spf13/cobra" | ||
| 9 | ) | ||
| 10 | |||
| 11 | var restartCmd = &cobra.Command{ | ||
| 12 | Use: "restart <app>", | ||
| 13 | Short: "Restart a deployment", | ||
| 14 | Args: cobra.ExactArgs(1), | ||
| 15 | RunE: runRestart, | ||
| 16 | } | ||
| 17 | |||
| 18 | func runRestart(cmd *cobra.Command, args []string) error { | ||
| 19 | name := args[0] | ||
| 20 | |||
| 21 | st, err := state.Load() | ||
| 22 | if err != nil { | ||
| 23 | return fmt.Errorf("error loading state: %w", err) | ||
| 24 | } | ||
| 25 | |||
| 26 | host := hostFlag | ||
| 27 | if host == "" { | ||
| 28 | host = st.GetDefaultHost() | ||
| 29 | } | ||
| 30 | |||
| 31 | if host == "" { | ||
| 32 | return fmt.Errorf("--host is required") | ||
| 33 | } | ||
| 34 | |||
| 35 | app, err := st.GetApp(host, name) | ||
| 36 | if err != nil { | ||
| 37 | return err | ||
| 38 | } | ||
| 39 | |||
| 40 | if app.Type != "app" { | ||
| 41 | return fmt.Errorf("restart is only available for apps, not static sites") | ||
| 42 | } | ||
| 43 | |||
| 44 | client, err := ssh.Connect(host) | ||
| 45 | if err != nil { | ||
| 46 | return fmt.Errorf("error connecting to VPS: %w", err) | ||
| 47 | } | ||
| 48 | defer client.Close() | ||
| 49 | |||
| 50 | fmt.Printf("Restarting %s...\n", name) | ||
| 51 | if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil { | ||
| 52 | return fmt.Errorf("error restarting service: %w", err) | ||
| 53 | } | ||
| 54 | |||
| 55 | fmt.Println("Service restarted successfully") | ||
| 56 | return nil | ||
| 57 | } | ||
diff --git a/cmd/deploy/root.go b/cmd/deploy/root.go deleted file mode 100644 index adbc7c8..0000000 --- a/cmd/deploy/root.go +++ /dev/null | |||
| @@ -1,377 +0,0 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "bufio" | ||
| 5 | "fmt" | ||
| 6 | "os" | ||
| 7 | "path/filepath" | ||
| 8 | "strconv" | ||
| 9 | "strings" | ||
| 10 | |||
| 11 | "github.com/bdw/deploy/internal/ssh" | ||
| 12 | "github.com/bdw/deploy/internal/state" | ||
| 13 | "github.com/bdw/deploy/internal/templates" | ||
| 14 | "github.com/spf13/cobra" | ||
| 15 | ) | ||
| 16 | |||
| 17 | func runDeploy(cmd *cobra.Command, args []string) error { | ||
| 18 | flags := cmd.Flags() | ||
| 19 | |||
| 20 | binary, _ := flags.GetString("binary") | ||
| 21 | static, _ := flags.GetBool("static") | ||
| 22 | dir, _ := flags.GetString("dir") | ||
| 23 | domain, _ := flags.GetString("domain") | ||
| 24 | name, _ := flags.GetString("name") | ||
| 25 | port, _ := flags.GetInt("port") | ||
| 26 | envVars, _ := flags.GetStringArray("env") | ||
| 27 | envFile, _ := flags.GetString("env-file") | ||
| 28 | binaryArgs, _ := flags.GetString("args") | ||
| 29 | files, _ := flags.GetStringArray("file") | ||
| 30 | |||
| 31 | // Get host from flag or state default | ||
| 32 | host := hostFlag | ||
| 33 | if host == "" { | ||
| 34 | st, err := state.Load() | ||
| 35 | if err != nil { | ||
| 36 | return fmt.Errorf("error loading state: %w", err) | ||
| 37 | } | ||
| 38 | host = st.GetDefaultHost() | ||
| 39 | } | ||
| 40 | |||
| 41 | // If no flags provided, show help | ||
| 42 | if domain == "" && binary == "" && !static { | ||
| 43 | return cmd.Help() | ||
| 44 | } | ||
| 45 | |||
| 46 | if host == "" || domain == "" { | ||
| 47 | return fmt.Errorf("--host and --domain are required") | ||
| 48 | } | ||
| 49 | |||
| 50 | if static { | ||
| 51 | return deployStatic(host, domain, name, dir) | ||
| 52 | } | ||
| 53 | return deployApp(host, domain, name, binary, port, envVars, envFile, binaryArgs, files) | ||
| 54 | } | ||
| 55 | |||
| 56 | func deployApp(host, domain, name, binaryPath string, portOverride int, envVars []string, envFile, args string, files []string) error { | ||
| 57 | if binaryPath == "" { | ||
| 58 | return fmt.Errorf("--binary is required") | ||
| 59 | } | ||
| 60 | |||
| 61 | if name == "" { | ||
| 62 | name = filepath.Base(binaryPath) | ||
| 63 | } | ||
| 64 | |||
| 65 | if _, err := os.Stat(binaryPath); err != nil { | ||
| 66 | return fmt.Errorf("binary not found: %s", binaryPath) | ||
| 67 | } | ||
| 68 | |||
| 69 | fmt.Printf("Deploying app: %s\n", name) | ||
| 70 | fmt.Printf(" Domain: %s\n", domain) | ||
| 71 | fmt.Printf(" Binary: %s\n", binaryPath) | ||
| 72 | |||
| 73 | st, err := state.Load() | ||
| 74 | if err != nil { | ||
| 75 | return fmt.Errorf("error loading state: %w", err) | ||
| 76 | } | ||
| 77 | |||
| 78 | existingApp, _ := st.GetApp(host, name) | ||
| 79 | var port int | ||
| 80 | if existingApp != nil { | ||
| 81 | port = existingApp.Port | ||
| 82 | fmt.Printf(" Updating existing deployment (port %d)\n", port) | ||
| 83 | } else { | ||
| 84 | if portOverride > 0 { | ||
| 85 | port = portOverride | ||
| 86 | } else { | ||
| 87 | port = st.AllocatePort(host) | ||
| 88 | } | ||
| 89 | fmt.Printf(" Allocated port: %d\n", port) | ||
| 90 | } | ||
| 91 | |||
| 92 | env := make(map[string]string) | ||
| 93 | if existingApp != nil { | ||
| 94 | for k, v := range existingApp.Env { | ||
| 95 | env[k] = v | ||
| 96 | } | ||
| 97 | if args == "" && existingApp.Args != "" { | ||
| 98 | args = existingApp.Args | ||
| 99 | } | ||
| 100 | if len(files) == 0 && len(existingApp.Files) > 0 { | ||
| 101 | files = existingApp.Files | ||
| 102 | } | ||
| 103 | } | ||
| 104 | |||
| 105 | for _, e := range envVars { | ||
| 106 | parts := strings.SplitN(e, "=", 2) | ||
| 107 | if len(parts) == 2 { | ||
| 108 | env[parts[0]] = parts[1] | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | if envFile != "" { | ||
| 113 | fileEnv, err := parseEnvFile(envFile) | ||
| 114 | if err != nil { | ||
| 115 | return fmt.Errorf("error reading env file: %w", err) | ||
| 116 | } | ||
| 117 | for k, v := range fileEnv { | ||
| 118 | env[k] = v | ||
| 119 | } | ||
| 120 | } | ||
| 121 | |||
| 122 | env["PORT"] = strconv.Itoa(port) | ||
| 123 | |||
| 124 | client, err := ssh.Connect(host) | ||
| 125 | if err != nil { | ||
| 126 | return fmt.Errorf("error connecting to VPS: %w", err) | ||
| 127 | } | ||
| 128 | defer client.Close() | ||
| 129 | |||
| 130 | fmt.Println("-> Uploading binary...") | ||
| 131 | remoteTmpPath := fmt.Sprintf("/tmp/%s", name) | ||
| 132 | if err := client.Upload(binaryPath, remoteTmpPath); err != nil { | ||
| 133 | return fmt.Errorf("error uploading binary: %w", err) | ||
| 134 | } | ||
| 135 | |||
| 136 | fmt.Println("-> Creating system user...") | ||
| 137 | client.RunSudo(fmt.Sprintf("useradd -r -s /bin/false %s", name)) | ||
| 138 | |||
| 139 | fmt.Println("-> Setting up directories...") | ||
| 140 | workDir := fmt.Sprintf("/var/lib/%s", name) | ||
| 141 | if _, err := client.RunSudo(fmt.Sprintf("mkdir -p %s", workDir)); err != nil { | ||
| 142 | return fmt.Errorf("error creating work directory: %w", err) | ||
| 143 | } | ||
| 144 | if _, err := client.RunSudo(fmt.Sprintf("chown %s:%s %s", name, name, workDir)); err != nil { | ||
| 145 | return fmt.Errorf("error setting work directory ownership: %w", err) | ||
| 146 | } | ||
| 147 | |||
| 148 | fmt.Println("-> Installing binary...") | ||
| 149 | binaryDest := fmt.Sprintf("/usr/local/bin/%s", name) | ||
| 150 | if _, err := client.RunSudo(fmt.Sprintf("mv %s %s", remoteTmpPath, binaryDest)); err != nil { | ||
| 151 | return fmt.Errorf("error moving binary: %w", err) | ||
| 152 | } | ||
| 153 | if _, err := client.RunSudo(fmt.Sprintf("chmod +x %s", binaryDest)); err != nil { | ||
| 154 | return fmt.Errorf("error making binary executable: %w", err) | ||
| 155 | } | ||
| 156 | |||
| 157 | if len(files) > 0 { | ||
| 158 | fmt.Println("-> Uploading config files...") | ||
| 159 | for _, file := range files { | ||
| 160 | if _, err := os.Stat(file); err != nil { | ||
| 161 | return fmt.Errorf("config file not found: %s", file) | ||
| 162 | } | ||
| 163 | |||
| 164 | remotePath := fmt.Sprintf("%s/%s", workDir, filepath.Base(file)) | ||
| 165 | remoteTmpPath := fmt.Sprintf("/tmp/%s_%s", name, filepath.Base(file)) | ||
| 166 | |||
| 167 | if err := client.Upload(file, remoteTmpPath); err != nil { | ||
| 168 | return fmt.Errorf("error uploading config file %s: %w", file, err) | ||
| 169 | } | ||
| 170 | |||
| 171 | if _, err := client.RunSudo(fmt.Sprintf("mv %s %s", remoteTmpPath, remotePath)); err != nil { | ||
| 172 | return fmt.Errorf("error moving config file %s: %w", file, err) | ||
| 173 | } | ||
| 174 | |||
| 175 | if _, err := client.RunSudo(fmt.Sprintf("chown %s:%s %s", name, name, remotePath)); err != nil { | ||
| 176 | return fmt.Errorf("error setting config file ownership %s: %w", file, err) | ||
| 177 | } | ||
| 178 | |||
| 179 | fmt.Printf(" Uploaded: %s\n", file) | ||
| 180 | } | ||
| 181 | } | ||
| 182 | |||
| 183 | fmt.Println("-> Creating environment file...") | ||
| 184 | envFilePath := fmt.Sprintf("/etc/deploy/env/%s.env", name) | ||
| 185 | envContent := "" | ||
| 186 | for k, v := range env { | ||
| 187 | envContent += fmt.Sprintf("%s=%s\n", k, v) | ||
| 188 | } | ||
| 189 | if err := client.WriteSudoFile(envFilePath, envContent); err != nil { | ||
| 190 | return fmt.Errorf("error creating env file: %w", err) | ||
| 191 | } | ||
| 192 | if _, err := client.RunSudo(fmt.Sprintf("chmod 600 %s", envFilePath)); err != nil { | ||
| 193 | return fmt.Errorf("error setting env file permissions: %w", err) | ||
| 194 | } | ||
| 195 | if _, err := client.RunSudo(fmt.Sprintf("chown %s:%s %s", name, name, envFilePath)); err != nil { | ||
| 196 | return fmt.Errorf("error setting env file ownership: %w", err) | ||
| 197 | } | ||
| 198 | |||
| 199 | fmt.Println("-> Creating systemd service...") | ||
| 200 | serviceContent, err := templates.SystemdService(map[string]string{ | ||
| 201 | "Name": name, | ||
| 202 | "User": name, | ||
| 203 | "WorkDir": workDir, | ||
| 204 | "BinaryPath": binaryDest, | ||
| 205 | "Port": strconv.Itoa(port), | ||
| 206 | "EnvFile": envFilePath, | ||
| 207 | "Args": args, | ||
| 208 | }) | ||
| 209 | if err != nil { | ||
| 210 | return fmt.Errorf("error generating systemd unit: %w", err) | ||
| 211 | } | ||
| 212 | |||
| 213 | servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", name) | ||
| 214 | if err := client.WriteSudoFile(servicePath, serviceContent); err != nil { | ||
| 215 | return fmt.Errorf("error creating systemd unit: %w", err) | ||
| 216 | } | ||
| 217 | |||
| 218 | fmt.Println("-> Configuring Caddy...") | ||
| 219 | caddyContent, err := templates.AppCaddy(map[string]string{ | ||
| 220 | "Domain": domain, | ||
| 221 | "Port": strconv.Itoa(port), | ||
| 222 | }) | ||
| 223 | if err != nil { | ||
| 224 | return fmt.Errorf("error generating Caddy config: %w", err) | ||
| 225 | } | ||
| 226 | |||
| 227 | caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) | ||
| 228 | if err := client.WriteSudoFile(caddyPath, caddyContent); err != nil { | ||
| 229 | return fmt.Errorf("error creating Caddy config: %w", err) | ||
| 230 | } | ||
| 231 | |||
| 232 | fmt.Println("-> Reloading systemd...") | ||
| 233 | if _, err := client.RunSudo("systemctl daemon-reload"); err != nil { | ||
| 234 | return fmt.Errorf("error reloading systemd: %w", err) | ||
| 235 | } | ||
| 236 | |||
| 237 | fmt.Println("-> Starting service...") | ||
| 238 | if _, err := client.RunSudo(fmt.Sprintf("systemctl enable %s", name)); err != nil { | ||
| 239 | return fmt.Errorf("error enabling service: %w", err) | ||
| 240 | } | ||
| 241 | if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil { | ||
| 242 | return fmt.Errorf("error starting service: %w", err) | ||
| 243 | } | ||
| 244 | |||
| 245 | fmt.Println("-> Reloading Caddy...") | ||
| 246 | if _, err := client.RunSudo("systemctl reload caddy"); err != nil { | ||
| 247 | return fmt.Errorf("error reloading Caddy: %w", err) | ||
| 248 | } | ||
| 249 | |||
| 250 | st.AddApp(host, name, &state.App{ | ||
| 251 | Type: "app", | ||
| 252 | Domain: domain, | ||
| 253 | Port: port, | ||
| 254 | Env: env, | ||
| 255 | Args: args, | ||
| 256 | Files: files, | ||
| 257 | }) | ||
| 258 | if err := st.Save(); err != nil { | ||
| 259 | return fmt.Errorf("error saving state: %w", err) | ||
| 260 | } | ||
| 261 | |||
| 262 | fmt.Printf("\n App deployed successfully!\n") | ||
| 263 | fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", domain) | ||
| 264 | return nil | ||
| 265 | } | ||
| 266 | |||
| 267 | func deployStatic(host, domain, name, dir string) error { | ||
| 268 | if name == "" { | ||
| 269 | name = domain | ||
| 270 | } | ||
| 271 | |||
| 272 | if _, err := os.Stat(dir); err != nil { | ||
| 273 | return fmt.Errorf("directory not found: %s", dir) | ||
| 274 | } | ||
| 275 | |||
| 276 | fmt.Printf("Deploying static site: %s\n", name) | ||
| 277 | fmt.Printf(" Domain: %s\n", domain) | ||
| 278 | fmt.Printf(" Directory: %s\n", dir) | ||
| 279 | |||
| 280 | st, err := state.Load() | ||
| 281 | if err != nil { | ||
| 282 | return fmt.Errorf("error loading state: %w", err) | ||
| 283 | } | ||
| 284 | |||
| 285 | client, err := ssh.Connect(host) | ||
| 286 | if err != nil { | ||
| 287 | return fmt.Errorf("error connecting to VPS: %w", err) | ||
| 288 | } | ||
| 289 | defer client.Close() | ||
| 290 | |||
| 291 | remoteDir := fmt.Sprintf("/var/www/%s", name) | ||
| 292 | fmt.Println("-> Creating remote directory...") | ||
| 293 | if _, err := client.RunSudo(fmt.Sprintf("mkdir -p %s", remoteDir)); err != nil { | ||
| 294 | return fmt.Errorf("error creating remote directory: %w", err) | ||
| 295 | } | ||
| 296 | |||
| 297 | currentUser, err := client.Run("whoami") | ||
| 298 | if err != nil { | ||
| 299 | return fmt.Errorf("error getting current user: %w", err) | ||
| 300 | } | ||
| 301 | currentUser = strings.TrimSpace(currentUser) | ||
| 302 | |||
| 303 | if _, err := client.RunSudo(fmt.Sprintf("chown -R %s:%s %s", currentUser, currentUser, remoteDir)); err != nil { | ||
| 304 | return fmt.Errorf("error setting temporary ownership: %w", err) | ||
| 305 | } | ||
| 306 | |||
| 307 | fmt.Println("-> Uploading files...") | ||
| 308 | if err := client.UploadDir(dir, remoteDir); err != nil { | ||
| 309 | return fmt.Errorf("error uploading files: %w", err) | ||
| 310 | } | ||
| 311 | |||
| 312 | fmt.Println("-> Setting permissions...") | ||
| 313 | if _, err := client.RunSudo(fmt.Sprintf("chown -R www-data:www-data %s", remoteDir)); err != nil { | ||
| 314 | return fmt.Errorf("error setting ownership: %w", err) | ||
| 315 | } | ||
| 316 | if _, err := client.RunSudo(fmt.Sprintf("chmod -R 755 %s", remoteDir)); err != nil { | ||
| 317 | return fmt.Errorf("error setting directory permissions: %w", err) | ||
| 318 | } | ||
| 319 | if _, err := client.RunSudo(fmt.Sprintf("find %s -type f -exec chmod 644 {} \\;", remoteDir)); err != nil { | ||
| 320 | return fmt.Errorf("error setting file permissions: %w", err) | ||
| 321 | } | ||
| 322 | |||
| 323 | fmt.Println("-> Configuring Caddy...") | ||
| 324 | caddyContent, err := templates.StaticCaddy(map[string]string{ | ||
| 325 | "Domain": domain, | ||
| 326 | "RootDir": remoteDir, | ||
| 327 | }) | ||
| 328 | if err != nil { | ||
| 329 | return fmt.Errorf("error generating Caddy config: %w", err) | ||
| 330 | } | ||
| 331 | |||
| 332 | caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) | ||
| 333 | if err := client.WriteSudoFile(caddyPath, caddyContent); err != nil { | ||
| 334 | return fmt.Errorf("error creating Caddy config: %w", err) | ||
| 335 | } | ||
| 336 | |||
| 337 | fmt.Println("-> Reloading Caddy...") | ||
| 338 | if _, err := client.RunSudo("systemctl reload caddy"); err != nil { | ||
| 339 | return fmt.Errorf("error reloading Caddy: %w", err) | ||
| 340 | } | ||
| 341 | |||
| 342 | st.AddApp(host, name, &state.App{ | ||
| 343 | Type: "static", | ||
| 344 | Domain: domain, | ||
| 345 | }) | ||
| 346 | if err := st.Save(); err != nil { | ||
| 347 | return fmt.Errorf("error saving state: %w", err) | ||
| 348 | } | ||
| 349 | |||
| 350 | fmt.Printf("\n Static site deployed successfully!\n") | ||
| 351 | fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", domain) | ||
| 352 | return nil | ||
| 353 | } | ||
| 354 | |||
| 355 | func parseEnvFile(path string) (map[string]string, error) { | ||
| 356 | file, err := os.Open(path) | ||
| 357 | if err != nil { | ||
| 358 | return nil, err | ||
| 359 | } | ||
| 360 | defer file.Close() | ||
| 361 | |||
| 362 | env := make(map[string]string) | ||
| 363 | scanner := bufio.NewScanner(file) | ||
| 364 | for scanner.Scan() { | ||
| 365 | line := strings.TrimSpace(scanner.Text()) | ||
| 366 | if line == "" || strings.HasPrefix(line, "#") { | ||
| 367 | continue | ||
| 368 | } | ||
| 369 | |||
| 370 | parts := strings.SplitN(line, "=", 2) | ||
| 371 | if len(parts) == 2 { | ||
| 372 | env[parts[0]] = parts[1] | ||
| 373 | } | ||
| 374 | } | ||
| 375 | |||
| 376 | return env, scanner.Err() | ||
| 377 | } | ||
diff --git a/cmd/deploy/status.go b/cmd/deploy/status.go deleted file mode 100644 index 4bcfc68..0000000 --- a/cmd/deploy/status.go +++ /dev/null | |||
| @@ -1,60 +0,0 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | |||
| 6 | "github.com/bdw/deploy/internal/ssh" | ||
| 7 | "github.com/bdw/deploy/internal/state" | ||
| 8 | "github.com/spf13/cobra" | ||
| 9 | ) | ||
| 10 | |||
| 11 | var statusCmd = &cobra.Command{ | ||
| 12 | Use: "status <app>", | ||
| 13 | Short: "Check status of a deployment", | ||
| 14 | Args: cobra.ExactArgs(1), | ||
| 15 | RunE: runStatus, | ||
| 16 | } | ||
| 17 | |||
| 18 | func runStatus(cmd *cobra.Command, args []string) error { | ||
| 19 | name := args[0] | ||
| 20 | |||
| 21 | st, err := state.Load() | ||
| 22 | if err != nil { | ||
| 23 | return fmt.Errorf("error loading state: %w", err) | ||
| 24 | } | ||
| 25 | |||
| 26 | host := hostFlag | ||
| 27 | if host == "" { | ||
| 28 | host = st.GetDefaultHost() | ||
| 29 | } | ||
| 30 | |||
| 31 | if host == "" { | ||
| 32 | return fmt.Errorf("--host is required") | ||
| 33 | } | ||
| 34 | |||
| 35 | app, err := st.GetApp(host, name) | ||
| 36 | if err != nil { | ||
| 37 | return err | ||
| 38 | } | ||
| 39 | |||
| 40 | if app.Type != "app" { | ||
| 41 | return fmt.Errorf("status is only available for apps, not static sites") | ||
| 42 | } | ||
| 43 | |||
| 44 | client, err := ssh.Connect(host) | ||
| 45 | if err != nil { | ||
| 46 | return fmt.Errorf("error connecting to VPS: %w", err) | ||
| 47 | } | ||
| 48 | defer client.Close() | ||
| 49 | |||
| 50 | output, err := client.RunSudo(fmt.Sprintf("systemctl status %s", name)) | ||
| 51 | if err != nil { | ||
| 52 | // systemctl status returns non-zero for non-active services | ||
| 53 | // but we still want to show the output | ||
| 54 | fmt.Print(output) | ||
| 55 | return nil | ||
| 56 | } | ||
| 57 | |||
| 58 | fmt.Print(output) | ||
| 59 | return nil | ||
| 60 | } | ||
diff --git a/cmd/deploy/templates/webui.html b/cmd/deploy/templates/webui.html deleted file mode 100644 index 052d599..0000000 --- a/cmd/deploy/templates/webui.html +++ /dev/null | |||
| @@ -1,440 +0,0 @@ | |||
| 1 | <!DOCTYPE html> | ||
| 2 | <html lang="en"> | ||
| 3 | <head> | ||
| 4 | <meta charset="UTF-8"> | ||
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| 6 | <title>Deploy - Web UI</title> | ||
| 7 | <style> | ||
| 8 | * { | ||
| 9 | margin: 0; | ||
| 10 | padding: 0; | ||
| 11 | box-sizing: border-box; | ||
| 12 | } | ||
| 13 | |||
| 14 | body { | ||
| 15 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | ||
| 16 | background: #f5f5f5; | ||
| 17 | color: #333; | ||
| 18 | line-height: 1.6; | ||
| 19 | } | ||
| 20 | |||
| 21 | header { | ||
| 22 | background: #2c3e50; | ||
| 23 | color: white; | ||
| 24 | padding: 1.5rem 2rem; | ||
| 25 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); | ||
| 26 | } | ||
| 27 | |||
| 28 | header h1 { | ||
| 29 | font-size: 1.8rem; | ||
| 30 | font-weight: 600; | ||
| 31 | } | ||
| 32 | |||
| 33 | header p { | ||
| 34 | color: #bdc3c7; | ||
| 35 | margin-top: 0.25rem; | ||
| 36 | font-size: 0.9rem; | ||
| 37 | } | ||
| 38 | |||
| 39 | .container { | ||
| 40 | max-width: 1200px; | ||
| 41 | margin: 2rem auto; | ||
| 42 | padding: 0 2rem; | ||
| 43 | } | ||
| 44 | |||
| 45 | .empty-state { | ||
| 46 | text-align: center; | ||
| 47 | padding: 4rem 2rem; | ||
| 48 | background: white; | ||
| 49 | border-radius: 8px; | ||
| 50 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); | ||
| 51 | } | ||
| 52 | |||
| 53 | .empty-state h2 { | ||
| 54 | color: #7f8c8d; | ||
| 55 | font-weight: 500; | ||
| 56 | margin-bottom: 0.5rem; | ||
| 57 | } | ||
| 58 | |||
| 59 | .empty-state p { | ||
| 60 | color: #95a5a6; | ||
| 61 | } | ||
| 62 | |||
| 63 | .host-section { | ||
| 64 | margin-bottom: 2rem; | ||
| 65 | } | ||
| 66 | |||
| 67 | .host-header { | ||
| 68 | background: white; | ||
| 69 | padding: 1rem 1.5rem; | ||
| 70 | border-radius: 8px 8px 0 0; | ||
| 71 | border-left: 4px solid #3498db; | ||
| 72 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); | ||
| 73 | } | ||
| 74 | |||
| 75 | .host-header h2 { | ||
| 76 | font-size: 1.3rem; | ||
| 77 | color: #2c3e50; | ||
| 78 | font-weight: 600; | ||
| 79 | } | ||
| 80 | |||
| 81 | .apps-grid { | ||
| 82 | display: grid; | ||
| 83 | grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); | ||
| 84 | gap: 1rem; | ||
| 85 | padding: 1rem; | ||
| 86 | background: #ecf0f1; | ||
| 87 | border-radius: 0 0 8px 8px; | ||
| 88 | } | ||
| 89 | |||
| 90 | .app-card { | ||
| 91 | background: white; | ||
| 92 | padding: 1.5rem; | ||
| 93 | border-radius: 6px; | ||
| 94 | box-shadow: 0 1px 3px rgba(0,0,0,0.1); | ||
| 95 | transition: transform 0.2s, box-shadow 0.2s; | ||
| 96 | } | ||
| 97 | |||
| 98 | .app-card:hover { | ||
| 99 | transform: translateY(-2px); | ||
| 100 | box-shadow: 0 4px 8px rgba(0,0,0,0.15); | ||
| 101 | } | ||
| 102 | |||
| 103 | .app-header { | ||
| 104 | display: flex; | ||
| 105 | justify-content: space-between; | ||
| 106 | align-items: center; | ||
| 107 | margin-bottom: 1rem; | ||
| 108 | } | ||
| 109 | |||
| 110 | .app-name { | ||
| 111 | font-size: 1.2rem; | ||
| 112 | font-weight: 600; | ||
| 113 | color: #2c3e50; | ||
| 114 | } | ||
| 115 | |||
| 116 | .app-type { | ||
| 117 | padding: 0.25rem 0.75rem; | ||
| 118 | border-radius: 12px; | ||
| 119 | font-size: 0.75rem; | ||
| 120 | font-weight: 500; | ||
| 121 | text-transform: uppercase; | ||
| 122 | } | ||
| 123 | |||
| 124 | .app-type.app { | ||
| 125 | background: #3498db; | ||
| 126 | color: white; | ||
| 127 | } | ||
| 128 | |||
| 129 | .app-type.static { | ||
| 130 | background: #2ecc71; | ||
| 131 | color: white; | ||
| 132 | } | ||
| 133 | |||
| 134 | .app-info { | ||
| 135 | margin-bottom: 0.5rem; | ||
| 136 | } | ||
| 137 | |||
| 138 | .app-info-label { | ||
| 139 | color: #7f8c8d; | ||
| 140 | font-size: 0.85rem; | ||
| 141 | font-weight: 500; | ||
| 142 | margin-bottom: 0.25rem; | ||
| 143 | } | ||
| 144 | |||
| 145 | .app-info-value { | ||
| 146 | color: #2c3e50; | ||
| 147 | font-family: 'Monaco', 'Courier New', monospace; | ||
| 148 | font-size: 0.9rem; | ||
| 149 | word-break: break-all; | ||
| 150 | } | ||
| 151 | |||
| 152 | .app-info-value a { | ||
| 153 | color: #3498db; | ||
| 154 | text-decoration: none; | ||
| 155 | } | ||
| 156 | |||
| 157 | .app-info-value a:hover { | ||
| 158 | text-decoration: underline; | ||
| 159 | } | ||
| 160 | |||
| 161 | .config-buttons { | ||
| 162 | margin-top: 1rem; | ||
| 163 | padding-top: 1rem; | ||
| 164 | border-top: 1px solid #ecf0f1; | ||
| 165 | display: flex; | ||
| 166 | gap: 0.5rem; | ||
| 167 | flex-wrap: wrap; | ||
| 168 | } | ||
| 169 | |||
| 170 | .config-btn { | ||
| 171 | padding: 0.4rem 0.8rem; | ||
| 172 | background: #3498db; | ||
| 173 | color: white; | ||
| 174 | border: none; | ||
| 175 | border-radius: 4px; | ||
| 176 | font-size: 0.8rem; | ||
| 177 | cursor: pointer; | ||
| 178 | transition: background 0.2s; | ||
| 179 | } | ||
| 180 | |||
| 181 | .config-btn:hover { | ||
| 182 | background: #2980b9; | ||
| 183 | } | ||
| 184 | |||
| 185 | .config-btn.secondary { | ||
| 186 | background: #95a5a6; | ||
| 187 | } | ||
| 188 | |||
| 189 | .config-btn.secondary:hover { | ||
| 190 | background: #7f8c8d; | ||
| 191 | } | ||
| 192 | |||
| 193 | .modal { | ||
| 194 | display: none; | ||
| 195 | position: fixed; | ||
| 196 | z-index: 1000; | ||
| 197 | left: 0; | ||
| 198 | top: 0; | ||
| 199 | width: 100%; | ||
| 200 | height: 100%; | ||
| 201 | overflow: auto; | ||
| 202 | background-color: rgba(0,0,0,0.6); | ||
| 203 | } | ||
| 204 | |||
| 205 | .modal.active { | ||
| 206 | display: block; | ||
| 207 | } | ||
| 208 | |||
| 209 | .modal-content { | ||
| 210 | background-color: #fefefe; | ||
| 211 | margin: 5% auto; | ||
| 212 | padding: 0; | ||
| 213 | border-radius: 8px; | ||
| 214 | width: 90%; | ||
| 215 | max-width: 900px; | ||
| 216 | max-height: 80vh; | ||
| 217 | display: flex; | ||
| 218 | flex-direction: column; | ||
| 219 | box-shadow: 0 4px 20px rgba(0,0,0,0.3); | ||
| 220 | } | ||
| 221 | |||
| 222 | .modal-header { | ||
| 223 | padding: 1.5rem; | ||
| 224 | border-bottom: 1px solid #ecf0f1; | ||
| 225 | display: flex; | ||
| 226 | justify-content: space-between; | ||
| 227 | align-items: center; | ||
| 228 | } | ||
| 229 | |||
| 230 | .modal-header h3 { | ||
| 231 | margin: 0; | ||
| 232 | color: #2c3e50; | ||
| 233 | } | ||
| 234 | |||
| 235 | .modal-path { | ||
| 236 | font-family: 'Monaco', 'Courier New', monospace; | ||
| 237 | font-size: 0.85rem; | ||
| 238 | color: #7f8c8d; | ||
| 239 | margin-top: 0.25rem; | ||
| 240 | } | ||
| 241 | |||
| 242 | .close { | ||
| 243 | color: #aaa; | ||
| 244 | font-size: 28px; | ||
| 245 | font-weight: bold; | ||
| 246 | cursor: pointer; | ||
| 247 | line-height: 1; | ||
| 248 | } | ||
| 249 | |||
| 250 | .close:hover { | ||
| 251 | color: #000; | ||
| 252 | } | ||
| 253 | |||
| 254 | .modal-body { | ||
| 255 | padding: 1.5rem; | ||
| 256 | overflow: auto; | ||
| 257 | flex: 1; | ||
| 258 | } | ||
| 259 | |||
| 260 | .config-content { | ||
| 261 | background: #282c34; | ||
| 262 | color: #abb2bf; | ||
| 263 | padding: 1rem; | ||
| 264 | border-radius: 4px; | ||
| 265 | font-family: 'Monaco', 'Courier New', monospace; | ||
| 266 | font-size: 0.85rem; | ||
| 267 | line-height: 1.5; | ||
| 268 | white-space: pre-wrap; | ||
| 269 | word-wrap: break-word; | ||
| 270 | overflow-x: auto; | ||
| 271 | text-align: left; | ||
| 272 | } | ||
| 273 | |||
| 274 | .loading { | ||
| 275 | text-align: center; | ||
| 276 | padding: 2rem; | ||
| 277 | color: #7f8c8d; | ||
| 278 | } | ||
| 279 | |||
| 280 | .refresh-info { | ||
| 281 | text-align: center; | ||
| 282 | color: #7f8c8d; | ||
| 283 | font-size: 0.9rem; | ||
| 284 | margin-top: 2rem; | ||
| 285 | padding: 1rem; | ||
| 286 | } | ||
| 287 | </style> | ||
| 288 | </head> | ||
| 289 | <body> | ||
| 290 | <header> | ||
| 291 | <h1>Deploy Web UI</h1> | ||
| 292 | <p>Manage your VPS deployments</p> | ||
| 293 | </header> | ||
| 294 | |||
| 295 | <div class="container"> | ||
| 296 | {{if not .Hosts}} | ||
| 297 | <div class="empty-state"> | ||
| 298 | <h2>No deployments found</h2> | ||
| 299 | <p>Use the CLI to deploy your first app or static site</p> | ||
| 300 | </div> | ||
| 301 | {{else}} | ||
| 302 | {{range .Hosts}} | ||
| 303 | <div class="host-section"> | ||
| 304 | <div class="host-header"> | ||
| 305 | <h2>{{.Host}}</h2> | ||
| 306 | </div> | ||
| 307 | <div class="apps-grid"> | ||
| 308 | {{range .Apps}} | ||
| 309 | <div class="app-card"> | ||
| 310 | <div class="app-header"> | ||
| 311 | <div class="app-name">{{.Name}}</div> | ||
| 312 | <div class="app-type {{.Type}}">{{.Type}}</div> | ||
| 313 | </div> | ||
| 314 | |||
| 315 | <div class="app-info"> | ||
| 316 | <div class="app-info-label">Domain</div> | ||
| 317 | <div class="app-info-value"> | ||
| 318 | <a href="https://{{.Domain}}" target="_blank">{{.Domain}}</a> | ||
| 319 | </div> | ||
| 320 | </div> | ||
| 321 | |||
| 322 | {{if eq .Type "app"}} | ||
| 323 | <div class="app-info"> | ||
| 324 | <div class="app-info-label">Port</div> | ||
| 325 | <div class="app-info-value">{{.Port}}</div> | ||
| 326 | </div> | ||
| 327 | {{end}} | ||
| 328 | |||
| 329 | <div class="config-buttons"> | ||
| 330 | {{if eq .Type "app"}} | ||
| 331 | <button class="config-btn" onclick="showConfig('{{.Host}}', '{{.Name}}', 'systemd')">Systemd Unit</button> | ||
| 332 | {{end}} | ||
| 333 | <button class="config-btn" onclick="showConfig('{{.Host}}', '{{.Name}}', 'caddy')">Caddy Config</button> | ||
| 334 | {{if .Env}} | ||
| 335 | <button class="config-btn secondary" onclick="showConfig('{{.Host}}', '{{.Name}}', 'env')">Environment</button> | ||
| 336 | {{end}} | ||
| 337 | </div> | ||
| 338 | </div> | ||
| 339 | {{end}} | ||
| 340 | </div> | ||
| 341 | </div> | ||
| 342 | {{end}} | ||
| 343 | {{end}} | ||
| 344 | |||
| 345 | <div class="refresh-info"> | ||
| 346 | Refresh the page to see latest changes | ||
| 347 | </div> | ||
| 348 | </div> | ||
| 349 | |||
| 350 | <!-- Modal --> | ||
| 351 | <div id="configModal" class="modal"> | ||
| 352 | <div class="modal-content"> | ||
| 353 | <div class="modal-header"> | ||
| 354 | <div> | ||
| 355 | <h3 id="modalTitle">Configuration</h3> | ||
| 356 | <div class="modal-path" id="modalPath"></div> | ||
| 357 | </div> | ||
| 358 | <span class="close" onclick="closeModal()">×</span> | ||
| 359 | </div> | ||
| 360 | <div class="modal-body"> | ||
| 361 | <div id="modalContent" class="loading">Loading...</div> | ||
| 362 | </div> | ||
| 363 | </div> | ||
| 364 | </div> | ||
| 365 | |||
| 366 | <script> | ||
| 367 | const modal = document.getElementById('configModal'); | ||
| 368 | const modalTitle = document.getElementById('modalTitle'); | ||
| 369 | const modalPath = document.getElementById('modalPath'); | ||
| 370 | const modalContent = document.getElementById('modalContent'); | ||
| 371 | |||
| 372 | function closeModal() { | ||
| 373 | modal.classList.remove('active'); | ||
| 374 | } | ||
| 375 | |||
| 376 | window.onclick = function(event) { | ||
| 377 | if (event.target == modal) { | ||
| 378 | closeModal(); | ||
| 379 | } | ||
| 380 | } | ||
| 381 | |||
| 382 | async function showConfig(host, app, type) { | ||
| 383 | modal.classList.add('active'); | ||
| 384 | modalContent.innerHTML = '<div class="loading">Loading...</div>'; | ||
| 385 | |||
| 386 | const titles = { | ||
| 387 | 'systemd': 'Systemd Service Unit', | ||
| 388 | 'caddy': 'Caddy Configuration', | ||
| 389 | 'env': 'Environment Variables' | ||
| 390 | }; | ||
| 391 | |||
| 392 | modalTitle.textContent = titles[type]; | ||
| 393 | |||
| 394 | try { | ||
| 395 | const response = await fetch(`/api/configs?host=${encodeURIComponent(host)}&app=${encodeURIComponent(app)}`); | ||
| 396 | if (!response.ok) { | ||
| 397 | throw new Error(`HTTP error! status: ${response.status}`); | ||
| 398 | } | ||
| 399 | const configs = await response.json(); | ||
| 400 | |||
| 401 | let content = ''; | ||
| 402 | let path = ''; | ||
| 403 | |||
| 404 | switch(type) { | ||
| 405 | case 'systemd': | ||
| 406 | content = configs.systemd || 'No systemd config available'; | ||
| 407 | path = configs.systemdPath || ''; | ||
| 408 | break; | ||
| 409 | case 'caddy': | ||
| 410 | content = configs.caddy || 'No Caddy config available'; | ||
| 411 | path = configs.caddyPath || ''; | ||
| 412 | break; | ||
| 413 | case 'env': | ||
| 414 | content = configs.env || 'No environment variables'; | ||
| 415 | path = configs.envPath || ''; | ||
| 416 | break; | ||
| 417 | } | ||
| 418 | |||
| 419 | modalPath.textContent = path; | ||
| 420 | modalContent.innerHTML = `<div class="config-content">${escapeHtml(content)}</div>`; | ||
| 421 | } catch (error) { | ||
| 422 | modalContent.innerHTML = `<div class="loading">Error loading config: ${error.message}</div>`; | ||
| 423 | } | ||
| 424 | } | ||
| 425 | |||
| 426 | function escapeHtml(text) { | ||
| 427 | const div = document.createElement('div'); | ||
| 428 | div.textContent = text; | ||
| 429 | return div.innerHTML; | ||
| 430 | } | ||
| 431 | |||
| 432 | // Close modal with Escape key | ||
| 433 | document.addEventListener('keydown', function(event) { | ||
| 434 | if (event.key === 'Escape') { | ||
| 435 | closeModal(); | ||
| 436 | } | ||
| 437 | }); | ||
| 438 | </script> | ||
| 439 | </body> | ||
| 440 | </html> | ||
diff --git a/cmd/deploy/ui.go b/cmd/deploy/ui.go deleted file mode 100644 index 2ca88e0..0000000 --- a/cmd/deploy/ui.go +++ /dev/null | |||
| @@ -1,199 +0,0 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "embed" | ||
| 5 | "encoding/json" | ||
| 6 | "fmt" | ||
| 7 | "html/template" | ||
| 8 | "net/http" | ||
| 9 | "sort" | ||
| 10 | "strconv" | ||
| 11 | |||
| 12 | "github.com/bdw/deploy/internal/state" | ||
| 13 | "github.com/bdw/deploy/internal/templates" | ||
| 14 | "github.com/spf13/cobra" | ||
| 15 | ) | ||
| 16 | |||
| 17 | //go:embed templates/*.html | ||
| 18 | var templatesFS embed.FS | ||
| 19 | |||
| 20 | var uiCmd = &cobra.Command{ | ||
| 21 | Use: "ui", | ||
| 22 | Short: "Launch web management UI", | ||
| 23 | RunE: runUI, | ||
| 24 | } | ||
| 25 | |||
| 26 | func init() { | ||
| 27 | uiCmd.Flags().StringP("port", "p", "8080", "Port to run the web UI on") | ||
| 28 | } | ||
| 29 | |||
| 30 | func runUI(cmd *cobra.Command, args []string) error { | ||
| 31 | port, _ := cmd.Flags().GetString("port") | ||
| 32 | |||
| 33 | tmpl, err := template.ParseFS(templatesFS, "templates/webui.html") | ||
| 34 | if err != nil { | ||
| 35 | return fmt.Errorf("error parsing template: %w", err) | ||
| 36 | } | ||
| 37 | |||
| 38 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | ||
| 39 | st, err := state.Load() | ||
| 40 | if err != nil { | ||
| 41 | http.Error(w, fmt.Sprintf("Error loading state: %v", err), http.StatusInternalServerError) | ||
| 42 | return | ||
| 43 | } | ||
| 44 | |||
| 45 | type AppData struct { | ||
| 46 | Name string | ||
| 47 | Type string | ||
| 48 | Domain string | ||
| 49 | Port int | ||
| 50 | Env map[string]string | ||
| 51 | Host string | ||
| 52 | } | ||
| 53 | |||
| 54 | type HostData struct { | ||
| 55 | Host string | ||
| 56 | Apps []AppData | ||
| 57 | } | ||
| 58 | |||
| 59 | var hosts []HostData | ||
| 60 | for hostName, host := range st.Hosts { | ||
| 61 | var apps []AppData | ||
| 62 | for appName, app := range host.Apps { | ||
| 63 | apps = append(apps, AppData{ | ||
| 64 | Name: appName, | ||
| 65 | Type: app.Type, | ||
| 66 | Domain: app.Domain, | ||
| 67 | Port: app.Port, | ||
| 68 | Env: app.Env, | ||
| 69 | Host: hostName, | ||
| 70 | }) | ||
| 71 | } | ||
| 72 | |||
| 73 | sort.Slice(apps, func(i, j int) bool { | ||
| 74 | return apps[i].Name < apps[j].Name | ||
| 75 | }) | ||
| 76 | |||
| 77 | hosts = append(hosts, HostData{ | ||
| 78 | Host: hostName, | ||
| 79 | Apps: apps, | ||
| 80 | }) | ||
| 81 | } | ||
| 82 | |||
| 83 | sort.Slice(hosts, func(i, j int) bool { | ||
| 84 | return hosts[i].Host < hosts[j].Host | ||
| 85 | }) | ||
| 86 | |||
| 87 | data := struct { | ||
| 88 | Hosts []HostData | ||
| 89 | }{ | ||
| 90 | Hosts: hosts, | ||
| 91 | } | ||
| 92 | |||
| 93 | if err := tmpl.Execute(w, data); err != nil { | ||
| 94 | http.Error(w, fmt.Sprintf("Error rendering template: %v", err), http.StatusInternalServerError) | ||
| 95 | return | ||
| 96 | } | ||
| 97 | }) | ||
| 98 | |||
| 99 | http.HandleFunc("/api/state", func(w http.ResponseWriter, r *http.Request) { | ||
| 100 | st, err := state.Load() | ||
| 101 | if err != nil { | ||
| 102 | http.Error(w, fmt.Sprintf("Error loading state: %v", err), http.StatusInternalServerError) | ||
| 103 | return | ||
| 104 | } | ||
| 105 | |||
| 106 | w.Header().Set("Content-Type", "application/json") | ||
| 107 | json.NewEncoder(w).Encode(st) | ||
| 108 | }) | ||
| 109 | |||
| 110 | http.HandleFunc("/api/configs", func(w http.ResponseWriter, r *http.Request) { | ||
| 111 | host := r.URL.Query().Get("host") | ||
| 112 | appName := r.URL.Query().Get("app") | ||
| 113 | |||
| 114 | if host == "" || appName == "" { | ||
| 115 | http.Error(w, "Missing host or app parameter", http.StatusBadRequest) | ||
| 116 | return | ||
| 117 | } | ||
| 118 | |||
| 119 | st, err := state.Load() | ||
| 120 | if err != nil { | ||
| 121 | http.Error(w, fmt.Sprintf("Error loading state: %v", err), http.StatusInternalServerError) | ||
| 122 | return | ||
| 123 | } | ||
| 124 | |||
| 125 | app, err := st.GetApp(host, appName) | ||
| 126 | if err != nil { | ||
| 127 | http.Error(w, fmt.Sprintf("App not found: %v", err), http.StatusNotFound) | ||
| 128 | return | ||
| 129 | } | ||
| 130 | |||
| 131 | configs := make(map[string]string) | ||
| 132 | |||
| 133 | if app.Env != nil && len(app.Env) > 0 { | ||
| 134 | envContent := "" | ||
| 135 | for k, v := range app.Env { | ||
| 136 | envContent += fmt.Sprintf("%s=%s\n", k, v) | ||
| 137 | } | ||
| 138 | configs["env"] = envContent | ||
| 139 | configs["envPath"] = fmt.Sprintf("/etc/deploy/env/%s.env", appName) | ||
| 140 | } | ||
| 141 | |||
| 142 | if app.Type == "app" { | ||
| 143 | workDir := fmt.Sprintf("/var/lib/%s", appName) | ||
| 144 | binaryPath := fmt.Sprintf("/usr/local/bin/%s", appName) | ||
| 145 | envFilePath := fmt.Sprintf("/etc/deploy/env/%s.env", appName) | ||
| 146 | |||
| 147 | serviceContent, err := templates.SystemdService(map[string]string{ | ||
| 148 | "Name": appName, | ||
| 149 | "User": appName, | ||
| 150 | "WorkDir": workDir, | ||
| 151 | "BinaryPath": binaryPath, | ||
| 152 | "Port": strconv.Itoa(app.Port), | ||
| 153 | "EnvFile": envFilePath, | ||
| 154 | "Args": app.Args, | ||
| 155 | }) | ||
| 156 | if err != nil { | ||
| 157 | http.Error(w, fmt.Sprintf("Error rendering systemd service: %v", err), http.StatusInternalServerError) | ||
| 158 | return | ||
| 159 | } | ||
| 160 | configs["systemd"] = serviceContent | ||
| 161 | configs["systemdPath"] = fmt.Sprintf("/etc/systemd/system/%s.service", appName) | ||
| 162 | |||
| 163 | caddyContent, err := templates.AppCaddy(map[string]string{ | ||
| 164 | "Domain": app.Domain, | ||
| 165 | "Port": strconv.Itoa(app.Port), | ||
| 166 | }) | ||
| 167 | if err != nil { | ||
| 168 | http.Error(w, fmt.Sprintf("Error rendering Caddy config: %v", err), http.StatusInternalServerError) | ||
| 169 | return | ||
| 170 | } | ||
| 171 | configs["caddy"] = caddyContent | ||
| 172 | configs["caddyPath"] = fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", appName) | ||
| 173 | } else if app.Type == "static" { | ||
| 174 | remoteDir := fmt.Sprintf("/var/www/%s", appName) | ||
| 175 | caddyContent, err := templates.StaticCaddy(map[string]string{ | ||
| 176 | "Domain": app.Domain, | ||
| 177 | "RootDir": remoteDir, | ||
| 178 | }) | ||
| 179 | if err != nil { | ||
| 180 | http.Error(w, fmt.Sprintf("Error rendering Caddy config: %v", err), http.StatusInternalServerError) | ||
| 181 | return | ||
| 182 | } | ||
| 183 | configs["caddy"] = caddyContent | ||
| 184 | configs["caddyPath"] = fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", appName) | ||
| 185 | } | ||
| 186 | |||
| 187 | w.Header().Set("Content-Type", "application/json") | ||
| 188 | json.NewEncoder(w).Encode(configs) | ||
| 189 | }) | ||
| 190 | |||
| 191 | addr := fmt.Sprintf("localhost:%s", port) | ||
| 192 | fmt.Printf("Starting web UI on http://%s\n", addr) | ||
| 193 | fmt.Printf("Press Ctrl+C to stop\n") | ||
| 194 | |||
| 195 | if err := http.ListenAndServe(addr, nil); err != nil { | ||
| 196 | return fmt.Errorf("error starting server: %w", err) | ||
| 197 | } | ||
| 198 | return nil | ||
| 199 | } | ||
diff --git a/cmd/deploy/version.go b/cmd/deploy/version.go deleted file mode 100644 index d2cd430..0000000 --- a/cmd/deploy/version.go +++ /dev/null | |||
| @@ -1,17 +0,0 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | |||
| 6 | "github.com/spf13/cobra" | ||
| 7 | ) | ||
| 8 | |||
| 9 | var versionCmd = &cobra.Command{ | ||
| 10 | Use: "version", | ||
| 11 | Short: "Show version information", | ||
| 12 | Run: func(cmd *cobra.Command, args []string) { | ||
| 13 | fmt.Printf("deploy version %s\n", version) | ||
| 14 | fmt.Printf(" commit: %s\n", commit) | ||
| 15 | fmt.Printf(" built: %s\n", date) | ||
| 16 | }, | ||
| 17 | } | ||
