summaryrefslogtreecommitdiffstats
path: root/cmd/deploy/env.go
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-01-23 20:54:46 -0800
committerbndw <ben@bdw.to>2026-01-23 20:54:46 -0800
commit98b9af372025595e8a4255538e2836e019311474 (patch)
tree0a26fa5a8a19ea8565da6d63e1f19c21fc170d12 /cmd/deploy/env.go
parent7fcb9dfa87310e91b527829ece9989decb6fda64 (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.go176
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 @@
1package main
2
3import (
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
14func 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
167func 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}