diff options
| author | bndw <ben@bdw.to> | 2026-01-23 20:54:46 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-01-23 20:54:46 -0800 |
| commit | 98b9af372025595e8a4255538e2836e019311474 (patch) | |
| tree | 0a26fa5a8a19ea8565da6d63e1f19c21fc170d12 /cmd/deploy/env.go | |
| parent | 7fcb9dfa87310e91b527829ece9989decb6fda64 (diff) | |
Add deploy command and fix static site naming
Static sites now default to using the domain as the name instead of
the source directory basename, preventing conflicts when multiple
sites use the same directory name (e.g., dist).
Also fixes .gitignore to not exclude cmd/deploy/ directory.
Diffstat (limited to 'cmd/deploy/env.go')
| -rw-r--r-- | cmd/deploy/env.go | 176 |
1 files changed, 176 insertions, 0 deletions
diff --git a/cmd/deploy/env.go b/cmd/deploy/env.go new file mode 100644 index 0000000..135fb77 --- /dev/null +++ b/cmd/deploy/env.go | |||
| @@ -0,0 +1,176 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "flag" | ||
| 5 | "fmt" | ||
| 6 | "os" | ||
| 7 | "strings" | ||
| 8 | |||
| 9 | "github.com/bdw/deploy/internal/config" | ||
| 10 | "github.com/bdw/deploy/internal/ssh" | ||
| 11 | "github.com/bdw/deploy/internal/state" | ||
| 12 | ) | ||
| 13 | |||
| 14 | func runEnv(args []string) { | ||
| 15 | fs := flag.NewFlagSet("env", flag.ExitOnError) | ||
| 16 | host := fs.String("host", "", "VPS host (SSH config alias or user@host)") | ||
| 17 | var setVars envFlags | ||
| 18 | fs.Var(&setVars, "set", "Set environment variable (KEY=VALUE, can be specified multiple times)") | ||
| 19 | var unsetVars envFlags | ||
| 20 | fs.Var(&unsetVars, "unset", "Unset environment variable (KEY, can be specified multiple times)") | ||
| 21 | envFile := fs.String("file", "", "Load environment from file") | ||
| 22 | fs.Parse(args) | ||
| 23 | |||
| 24 | if len(fs.Args()) == 0 { | ||
| 25 | fmt.Fprintf(os.Stderr, "Error: app name is required\n") | ||
| 26 | fmt.Fprintf(os.Stderr, "Usage: deploy env <app-name> [--set KEY=VALUE] [--unset KEY] [--file .env] --host user@vps-ip\n") | ||
| 27 | os.Exit(1) | ||
| 28 | } | ||
| 29 | |||
| 30 | name := fs.Args()[0] | ||
| 31 | |||
| 32 | // Get host from flag or config | ||
| 33 | if *host == "" { | ||
| 34 | cfg, err := config.Load() | ||
| 35 | if err != nil { | ||
| 36 | fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) | ||
| 37 | os.Exit(1) | ||
| 38 | } | ||
| 39 | *host = cfg.Host | ||
| 40 | } | ||
| 41 | |||
| 42 | if *host == "" { | ||
| 43 | fmt.Fprintf(os.Stderr, "Error: --host is required\n") | ||
| 44 | fs.Usage() | ||
| 45 | os.Exit(1) | ||
| 46 | } | ||
| 47 | |||
| 48 | // Load state | ||
| 49 | st, err := state.Load() | ||
| 50 | if err != nil { | ||
| 51 | fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) | ||
| 52 | os.Exit(1) | ||
| 53 | } | ||
| 54 | |||
| 55 | // Get app info | ||
| 56 | app, err := st.GetApp(*host, name) | ||
| 57 | if err != nil { | ||
| 58 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) | ||
| 59 | os.Exit(1) | ||
| 60 | } | ||
| 61 | |||
| 62 | if app.Type != "app" { | ||
| 63 | fmt.Fprintf(os.Stderr, "Error: env is only available for apps, not static sites\n") | ||
| 64 | os.Exit(1) | ||
| 65 | } | ||
| 66 | |||
| 67 | // If no flags, just display current env (masked) | ||
| 68 | if len(setVars) == 0 && len(unsetVars) == 0 && *envFile == "" { | ||
| 69 | fmt.Printf("Environment variables for %s:\n\n", name) | ||
| 70 | if len(app.Env) == 0 { | ||
| 71 | fmt.Println(" (none)") | ||
| 72 | } else { | ||
| 73 | for k, v := range app.Env { | ||
| 74 | // Mask sensitive-looking values | ||
| 75 | display := v | ||
| 76 | if isSensitive(k) { | ||
| 77 | display = "***" | ||
| 78 | } | ||
| 79 | fmt.Printf(" %s=%s\n", k, display) | ||
| 80 | } | ||
| 81 | } | ||
| 82 | return | ||
| 83 | } | ||
| 84 | |||
| 85 | // Initialize env if nil | ||
| 86 | if app.Env == nil { | ||
| 87 | app.Env = make(map[string]string) | ||
| 88 | } | ||
| 89 | |||
| 90 | // Apply changes | ||
| 91 | changed := false | ||
| 92 | |||
| 93 | // Unset variables | ||
| 94 | for _, key := range unsetVars { | ||
| 95 | if _, exists := app.Env[key]; exists { | ||
| 96 | delete(app.Env, key) | ||
| 97 | changed = true | ||
| 98 | fmt.Printf("Unset %s\n", key) | ||
| 99 | } | ||
| 100 | } | ||
| 101 | |||
| 102 | // Set variables from flags | ||
| 103 | for _, e := range setVars { | ||
| 104 | parts := strings.SplitN(e, "=", 2) | ||
| 105 | if len(parts) == 2 { | ||
| 106 | app.Env[parts[0]] = parts[1] | ||
| 107 | changed = true | ||
| 108 | fmt.Printf("Set %s\n", parts[0]) | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | // Set variables from file | ||
| 113 | if *envFile != "" { | ||
| 114 | fileEnv, err := parseEnvFile(*envFile) | ||
| 115 | if err != nil { | ||
| 116 | fmt.Fprintf(os.Stderr, "Error reading env file: %v\n", err) | ||
| 117 | os.Exit(1) | ||
| 118 | } | ||
| 119 | for k, v := range fileEnv { | ||
| 120 | app.Env[k] = v | ||
| 121 | changed = true | ||
| 122 | fmt.Printf("Set %s\n", k) | ||
| 123 | } | ||
| 124 | } | ||
| 125 | |||
| 126 | if !changed { | ||
| 127 | fmt.Println("No changes made") | ||
| 128 | return | ||
| 129 | } | ||
| 130 | |||
| 131 | // Save state | ||
| 132 | if err := st.Save(); err != nil { | ||
| 133 | fmt.Fprintf(os.Stderr, "Error saving state: %v\n", err) | ||
| 134 | os.Exit(1) | ||
| 135 | } | ||
| 136 | |||
| 137 | // Connect to VPS and update env file | ||
| 138 | client, err := ssh.Connect(*host) | ||
| 139 | if err != nil { | ||
| 140 | fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) | ||
| 141 | os.Exit(1) | ||
| 142 | } | ||
| 143 | defer client.Close() | ||
| 144 | |||
| 145 | // Regenerate env file | ||
| 146 | fmt.Println("→ Updating environment file on VPS...") | ||
| 147 | envFilePath := fmt.Sprintf("/etc/deploy/env/%s.env", name) | ||
| 148 | envContent := "" | ||
| 149 | for k, v := range app.Env { | ||
| 150 | envContent += fmt.Sprintf("%s=%s\n", k, v) | ||
| 151 | } | ||
| 152 | if err := client.WriteSudoFile(envFilePath, envContent); err != nil { | ||
| 153 | fmt.Fprintf(os.Stderr, "Error updating env file: %v\n", err) | ||
| 154 | os.Exit(1) | ||
| 155 | } | ||
| 156 | |||
| 157 | // Restart service to pick up new env | ||
| 158 | fmt.Println("→ Restarting service...") | ||
| 159 | if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil { | ||
| 160 | fmt.Fprintf(os.Stderr, "Error restarting service: %v\n", err) | ||
| 161 | os.Exit(1) | ||
| 162 | } | ||
| 163 | |||
| 164 | fmt.Println("✓ Environment variables updated successfully") | ||
| 165 | } | ||
| 166 | |||
| 167 | func isSensitive(key string) bool { | ||
| 168 | key = strings.ToLower(key) | ||
| 169 | sensitiveWords := []string{"key", "secret", "password", "token", "api"} | ||
| 170 | for _, word := range sensitiveWords { | ||
| 171 | if strings.Contains(key, word) { | ||
| 172 | return true | ||
| 173 | } | ||
| 174 | } | ||
| 175 | return false | ||
| 176 | } | ||
