summaryrefslogtreecommitdiffstats
path: root/cmd/deploy/root.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/deploy/root.go')
-rw-r--r--cmd/deploy/root.go377
1 files changed, 0 insertions, 377 deletions
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 @@
1package main
2
3import (
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
17func 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
56func 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
267func 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
355func 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}