diff options
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/ship/root.go | 395 |
1 files changed, 215 insertions, 180 deletions
diff --git a/cmd/ship/root.go b/cmd/ship/root.go index c46491f..24eed1e 100644 --- a/cmd/ship/root.go +++ b/cmd/ship/root.go | |||
| @@ -16,100 +16,223 @@ import ( | |||
| 16 | 16 | ||
| 17 | // DeployOptions contains all options for deploying or updating an app | 17 | // DeployOptions contains all options for deploying or updating an app |
| 18 | type DeployOptions struct { | 18 | type DeployOptions struct { |
| 19 | Host string | 19 | Host string |
| 20 | Domain string | 20 | Domain string |
| 21 | Name string | 21 | Name string |
| 22 | Binary string | 22 | Binary string |
| 23 | Port int | 23 | Dir string // for static sites |
| 24 | EnvVars []string | 24 | Port int |
| 25 | EnvFile string | 25 | Args string |
| 26 | Args string | 26 | Files []string |
| 27 | Files []string | 27 | Memory string |
| 28 | Memory string | 28 | CPU string |
| 29 | CPU string | 29 | Env map[string]string // merged env vars |
| 30 | IsUpdate bool | ||
| 30 | } | 31 | } |
| 31 | 32 | ||
| 32 | func runDeploy(cmd *cobra.Command, args []string) error { | 33 | func runDeploy(cmd *cobra.Command, args []string) error { |
| 33 | flags := cmd.Flags() | 34 | flags := cmd.Flags() |
| 34 | 35 | ||
| 35 | opts := DeployOptions{} | 36 | // Parse CLI flags |
| 36 | opts.Binary, _ = flags.GetString("binary") | 37 | binary, _ := flags.GetString("binary") |
| 37 | static, _ := flags.GetBool("static") | 38 | static, _ := flags.GetBool("static") |
| 38 | dir, _ := flags.GetString("dir") | 39 | dir, _ := flags.GetString("dir") |
| 39 | opts.Domain, _ = flags.GetString("domain") | 40 | domain, _ := flags.GetString("domain") |
| 40 | opts.Name, _ = flags.GetString("name") | 41 | name, _ := flags.GetString("name") |
| 41 | opts.Port, _ = flags.GetInt("port") | 42 | port, _ := flags.GetInt("port") |
| 42 | opts.EnvVars, _ = flags.GetStringArray("env") | 43 | envVars, _ := flags.GetStringArray("env") |
| 43 | opts.EnvFile, _ = flags.GetString("env-file") | 44 | envFile, _ := flags.GetString("env-file") |
| 44 | opts.Args, _ = flags.GetString("args") | 45 | argsFlag, _ := flags.GetString("args") |
| 45 | opts.Files, _ = flags.GetStringArray("file") | 46 | files, _ := flags.GetStringArray("file") |
| 46 | opts.Memory, _ = flags.GetString("memory") | 47 | memory, _ := flags.GetString("memory") |
| 47 | opts.CPU, _ = flags.GetString("cpu") | 48 | cpu, _ := flags.GetString("cpu") |
| 48 | 49 | ||
| 49 | // Get host from flag or state default | 50 | // Get host from flag or state default |
| 50 | opts.Host = hostFlag | 51 | host := hostFlag |
| 51 | if opts.Host == "" { | 52 | if host == "" { |
| 52 | st, err := state.Load() | 53 | st, err := state.Load() |
| 53 | if err != nil { | 54 | if err != nil { |
| 54 | return fmt.Errorf("error loading state: %w", err) | 55 | return fmt.Errorf("error loading state: %w", err) |
| 55 | } | 56 | } |
| 56 | opts.Host = st.GetDefaultHost() | 57 | host = st.GetDefaultHost() |
| 57 | } | 58 | } |
| 58 | 59 | ||
| 59 | // If no flags provided, show help | 60 | // If no flags provided, show help |
| 60 | if opts.Domain == "" && opts.Binary == "" && !static && opts.Name == "" { | 61 | if domain == "" && binary == "" && !static && name == "" { |
| 61 | return cmd.Help() | 62 | return cmd.Help() |
| 62 | } | 63 | } |
| 63 | 64 | ||
| 64 | if opts.Host == "" { | 65 | if host == "" { |
| 65 | return fmt.Errorf("--host is required") | 66 | return fmt.Errorf("--host is required") |
| 66 | } | 67 | } |
| 67 | 68 | ||
| 68 | // Load state to check base domain | 69 | // Load state once - this will be used throughout |
| 69 | st, err := state.Load() | 70 | st, err := state.Load() |
| 70 | if err != nil { | 71 | if err != nil { |
| 71 | return fmt.Errorf("error loading state: %w", err) | 72 | return fmt.Errorf("error loading state: %w", err) |
| 72 | } | 73 | } |
| 73 | hostState := st.GetHost(opts.Host) | 74 | hostState := st.GetHost(host) |
| 74 | 75 | ||
| 75 | // Config update mode: --name provided without --binary or --static | 76 | // Config update mode: --name provided without --binary or --static |
| 76 | if opts.Name != "" && opts.Binary == "" && !static { | 77 | if name != "" && binary == "" && !static { |
| 77 | return updateAppConfig(opts) | 78 | existingApp, err := st.GetApp(host, name) |
| 78 | } | 79 | if err != nil { |
| 80 | return fmt.Errorf("app %s not found (use --binary to deploy a new app)", name) | ||
| 81 | } | ||
| 79 | 82 | ||
| 80 | if opts.Domain == "" && hostState.BaseDomain == "" { | 83 | // Build merged config starting from existing app |
| 81 | return fmt.Errorf("--domain required (or configure base domain with 'ship host init --base-domain')") | 84 | opts := DeployOptions{ |
| 85 | Host: host, | ||
| 86 | Name: name, | ||
| 87 | Port: existingApp.Port, | ||
| 88 | Args: existingApp.Args, | ||
| 89 | Files: existingApp.Files, | ||
| 90 | Memory: existingApp.Memory, | ||
| 91 | CPU: existingApp.CPU, | ||
| 92 | Env: make(map[string]string), | ||
| 93 | } | ||
| 94 | for k, v := range existingApp.Env { | ||
| 95 | opts.Env[k] = v | ||
| 96 | } | ||
| 97 | |||
| 98 | // Override with CLI flags if provided | ||
| 99 | if argsFlag != "" { | ||
| 100 | opts.Args = argsFlag | ||
| 101 | } | ||
| 102 | if len(files) > 0 { | ||
| 103 | opts.Files = files | ||
| 104 | } | ||
| 105 | if memory != "" { | ||
| 106 | opts.Memory = memory | ||
| 107 | } | ||
| 108 | if cpu != "" { | ||
| 109 | opts.CPU = cpu | ||
| 110 | } | ||
| 111 | |||
| 112 | // Merge env vars (CLI overrides existing) | ||
| 113 | for _, e := range envVars { | ||
| 114 | parts := strings.SplitN(e, "=", 2) | ||
| 115 | if len(parts) == 2 { | ||
| 116 | opts.Env[parts[0]] = parts[1] | ||
| 117 | } | ||
| 118 | } | ||
| 119 | if envFile != "" { | ||
| 120 | fileEnv, err := parseEnvFile(envFile) | ||
| 121 | if err != nil { | ||
| 122 | return fmt.Errorf("error reading env file: %w", err) | ||
| 123 | } | ||
| 124 | for k, v := range fileEnv { | ||
| 125 | opts.Env[k] = v | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | return updateAppConfig(st, opts) | ||
| 82 | } | 130 | } |
| 83 | 131 | ||
| 84 | // Infer name early so we can use it for subdomain generation | 132 | // Infer name early so we can use it for subdomain generation and existing app lookup |
| 85 | if opts.Name == "" { | 133 | if name == "" { |
| 86 | if static { | 134 | if static { |
| 87 | opts.Name = opts.Domain | 135 | name = domain |
| 88 | if opts.Name == "" && hostState.BaseDomain != "" { | 136 | if name == "" && hostState.BaseDomain != "" { |
| 89 | opts.Name = filepath.Base(dir) | 137 | name = filepath.Base(dir) |
| 90 | } | 138 | } |
| 91 | } else { | 139 | } else { |
| 92 | opts.Name = filepath.Base(opts.Binary) | 140 | name = filepath.Base(binary) |
| 93 | } | 141 | } |
| 94 | } | 142 | } |
| 95 | 143 | ||
| 96 | // Generate subdomain if base domain configured | 144 | // Check if this is an update to an existing app/site |
| 145 | existingApp, _ := st.GetApp(host, name) | ||
| 146 | isUpdate := existingApp != nil | ||
| 147 | |||
| 148 | // For new deployments, require domain or base domain | ||
| 149 | if !isUpdate && domain == "" && hostState.BaseDomain == "" { | ||
| 150 | return fmt.Errorf("--domain required (or configure base domain with 'ship host init --base-domain')") | ||
| 151 | } | ||
| 152 | |||
| 153 | // Build merged config, starting from existing app if updating | ||
| 154 | opts := DeployOptions{ | ||
| 155 | Host: host, | ||
| 156 | Name: name, | ||
| 157 | Binary: binary, | ||
| 158 | Dir: dir, | ||
| 159 | IsUpdate: isUpdate, | ||
| 160 | } | ||
| 161 | |||
| 162 | // Merge domain: auto-subdomain + (user-provided or existing custom domain) | ||
| 97 | var domains []string | 163 | var domains []string |
| 98 | if hostState.BaseDomain != "" { | 164 | if hostState.BaseDomain != "" { |
| 99 | domains = append(domains, opts.Name+"."+hostState.BaseDomain) | 165 | domains = append(domains, name+"."+hostState.BaseDomain) |
| 100 | } | 166 | } |
| 101 | if opts.Domain != "" { | 167 | if domain != "" { |
| 102 | domains = append(domains, opts.Domain) | 168 | domains = append(domains, domain) |
| 169 | } else if isUpdate && existingApp.Domain != "" { | ||
| 170 | for _, d := range strings.Split(existingApp.Domain, ",") { | ||
| 171 | d = strings.TrimSpace(d) | ||
| 172 | if d != "" && (hostState.BaseDomain == "" || !strings.HasSuffix(d, "."+hostState.BaseDomain)) { | ||
| 173 | domains = append(domains, d) | ||
| 174 | } | ||
| 175 | } | ||
| 103 | } | 176 | } |
| 104 | opts.Domain = strings.Join(domains, ", ") | 177 | opts.Domain = strings.Join(domains, ", ") |
| 105 | 178 | ||
| 179 | // For apps, merge all config fields | ||
| 180 | if !static { | ||
| 181 | // Start with existing values if updating | ||
| 182 | if isUpdate { | ||
| 183 | opts.Port = existingApp.Port | ||
| 184 | opts.Args = existingApp.Args | ||
| 185 | opts.Files = existingApp.Files | ||
| 186 | opts.Memory = existingApp.Memory | ||
| 187 | opts.CPU = existingApp.CPU | ||
| 188 | opts.Env = make(map[string]string) | ||
| 189 | for k, v := range existingApp.Env { | ||
| 190 | opts.Env[k] = v | ||
| 191 | } | ||
| 192 | } else { | ||
| 193 | opts.Port = port | ||
| 194 | opts.Env = make(map[string]string) | ||
| 195 | } | ||
| 196 | |||
| 197 | // Override with CLI flags if provided | ||
| 198 | if argsFlag != "" { | ||
| 199 | opts.Args = argsFlag | ||
| 200 | } | ||
| 201 | if len(files) > 0 { | ||
| 202 | opts.Files = files | ||
| 203 | } | ||
| 204 | if memory != "" { | ||
| 205 | opts.Memory = memory | ||
| 206 | } | ||
| 207 | if cpu != "" { | ||
| 208 | opts.CPU = cpu | ||
| 209 | } | ||
| 210 | |||
| 211 | // Merge env vars (CLI overrides existing) | ||
| 212 | for _, e := range envVars { | ||
| 213 | parts := strings.SplitN(e, "=", 2) | ||
| 214 | if len(parts) == 2 { | ||
| 215 | opts.Env[parts[0]] = parts[1] | ||
| 216 | } | ||
| 217 | } | ||
| 218 | if envFile != "" { | ||
| 219 | fileEnv, err := parseEnvFile(envFile) | ||
| 220 | if err != nil { | ||
| 221 | return fmt.Errorf("error reading env file: %w", err) | ||
| 222 | } | ||
| 223 | for k, v := range fileEnv { | ||
| 224 | opts.Env[k] = v | ||
| 225 | } | ||
| 226 | } | ||
| 227 | } | ||
| 228 | |||
| 106 | if static { | 229 | if static { |
| 107 | return deployStatic(opts.Host, opts.Domain, opts.Name, dir) | 230 | return deployStatic(st, opts) |
| 108 | } | 231 | } |
| 109 | return deployApp(opts) | 232 | return deployApp(st, opts) |
| 110 | } | 233 | } |
| 111 | 234 | ||
| 112 | func deployApp(opts DeployOptions) error { | 235 | func deployApp(st *state.State, opts DeployOptions) error { |
| 113 | if opts.Binary == "" { | 236 | if opts.Binary == "" { |
| 114 | return fmt.Errorf("--binary is required") | 237 | return fmt.Errorf("--binary is required") |
| 115 | } | 238 | } |
| @@ -122,67 +245,19 @@ func deployApp(opts DeployOptions) error { | |||
| 122 | fmt.Printf(" Domain(s): %s\n", opts.Domain) | 245 | fmt.Printf(" Domain(s): %s\n", opts.Domain) |
| 123 | fmt.Printf(" Binary: %s\n", opts.Binary) | 246 | fmt.Printf(" Binary: %s\n", opts.Binary) |
| 124 | 247 | ||
| 125 | st, err := state.Load() | 248 | // Allocate port for new apps |
| 126 | if err != nil { | 249 | port := opts.Port |
| 127 | return fmt.Errorf("error loading state: %w", err) | 250 | if opts.IsUpdate { |
| 128 | } | ||
| 129 | |||
| 130 | existingApp, _ := st.GetApp(opts.Host, opts.Name) | ||
| 131 | var port int | ||
| 132 | if existingApp != nil { | ||
| 133 | port = existingApp.Port | ||
| 134 | fmt.Printf(" Updating existing deployment (port %d)\n", port) | 251 | fmt.Printf(" Updating existing deployment (port %d)\n", port) |
| 135 | } else { | 252 | } else { |
| 136 | if opts.Port > 0 { | 253 | if port == 0 { |
| 137 | port = opts.Port | ||
| 138 | } else { | ||
| 139 | port = st.AllocatePort(opts.Host) | 254 | port = st.AllocatePort(opts.Host) |
| 140 | } | 255 | } |
| 141 | fmt.Printf(" Allocated port: %d\n", port) | 256 | fmt.Printf(" Allocated port: %d\n", port) |
| 142 | } | 257 | } |
| 143 | 258 | ||
| 144 | // Merge with existing config | 259 | // Add PORT to env |
| 145 | args := opts.Args | 260 | opts.Env["PORT"] = strconv.Itoa(port) |
| 146 | files := opts.Files | ||
| 147 | memory := opts.Memory | ||
| 148 | cpu := opts.CPU | ||
| 149 | env := make(map[string]string) | ||
| 150 | if existingApp != nil { | ||
| 151 | for k, v := range existingApp.Env { | ||
| 152 | env[k] = v | ||
| 153 | } | ||
| 154 | if args == "" && existingApp.Args != "" { | ||
| 155 | args = existingApp.Args | ||
| 156 | } | ||
| 157 | if len(files) == 0 && len(existingApp.Files) > 0 { | ||
| 158 | files = existingApp.Files | ||
| 159 | } | ||
| 160 | if memory == "" && existingApp.Memory != "" { | ||
| 161 | memory = existingApp.Memory | ||
| 162 | } | ||
| 163 | if cpu == "" && existingApp.CPU != "" { | ||
| 164 | cpu = existingApp.CPU | ||
| 165 | } | ||
| 166 | } | ||
| 167 | |||
| 168 | for _, e := range opts.EnvVars { | ||
| 169 | parts := strings.SplitN(e, "=", 2) | ||
| 170 | if len(parts) == 2 { | ||
| 171 | env[parts[0]] = parts[1] | ||
| 172 | } | ||
| 173 | } | ||
| 174 | |||
| 175 | if opts.EnvFile != "" { | ||
| 176 | fileEnv, err := parseEnvFile(opts.EnvFile) | ||
| 177 | if err != nil { | ||
| 178 | return fmt.Errorf("error reading env file: %w", err) | ||
| 179 | } | ||
| 180 | for k, v := range fileEnv { | ||
| 181 | env[k] = v | ||
| 182 | } | ||
| 183 | } | ||
| 184 | |||
| 185 | env["PORT"] = strconv.Itoa(port) | ||
| 186 | 261 | ||
| 187 | client, err := ssh.Connect(opts.Host) | 262 | client, err := ssh.Connect(opts.Host) |
| 188 | if err != nil { | 263 | if err != nil { |
| @@ -217,9 +292,9 @@ func deployApp(opts DeployOptions) error { | |||
| 217 | return fmt.Errorf("error making binary executable: %w", err) | 292 | return fmt.Errorf("error making binary executable: %w", err) |
| 218 | } | 293 | } |
| 219 | 294 | ||
| 220 | if len(files) > 0 { | 295 | if len(opts.Files) > 0 { |
| 221 | fmt.Println("-> Uploading config files...") | 296 | fmt.Println("-> Uploading config files...") |
| 222 | for _, file := range files { | 297 | for _, file := range opts.Files { |
| 223 | if _, err := os.Stat(file); err != nil { | 298 | if _, err := os.Stat(file); err != nil { |
| 224 | return fmt.Errorf("config file not found: %s", file) | 299 | return fmt.Errorf("config file not found: %s", file) |
| 225 | } | 300 | } |
| @@ -246,7 +321,7 @@ func deployApp(opts DeployOptions) error { | |||
| 246 | fmt.Println("-> Creating environment file...") | 321 | fmt.Println("-> Creating environment file...") |
| 247 | envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", opts.Name) | 322 | envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", opts.Name) |
| 248 | envContent := "" | 323 | envContent := "" |
| 249 | for k, v := range env { | 324 | for k, v := range opts.Env { |
| 250 | envContent += fmt.Sprintf("%s=%s\n", k, v) | 325 | envContent += fmt.Sprintf("%s=%s\n", k, v) |
| 251 | } | 326 | } |
| 252 | if err := client.WriteSudoFile(envFilePath, envContent); err != nil { | 327 | if err := client.WriteSudoFile(envFilePath, envContent); err != nil { |
| @@ -267,9 +342,9 @@ func deployApp(opts DeployOptions) error { | |||
| 267 | "BinaryPath": binaryDest, | 342 | "BinaryPath": binaryDest, |
| 268 | "Port": strconv.Itoa(port), | 343 | "Port": strconv.Itoa(port), |
| 269 | "EnvFile": envFilePath, | 344 | "EnvFile": envFilePath, |
| 270 | "Args": args, | 345 | "Args": opts.Args, |
| 271 | "Memory": memory, | 346 | "Memory": opts.Memory, |
| 272 | "CPU": cpu, | 347 | "CPU": opts.CPU, |
| 273 | }) | 348 | }) |
| 274 | if err != nil { | 349 | if err != nil { |
| 275 | return fmt.Errorf("error generating systemd unit: %w", err) | 350 | return fmt.Errorf("error generating systemd unit: %w", err) |
| @@ -316,11 +391,11 @@ func deployApp(opts DeployOptions) error { | |||
| 316 | Type: "app", | 391 | Type: "app", |
| 317 | Domain: opts.Domain, | 392 | Domain: opts.Domain, |
| 318 | Port: port, | 393 | Port: port, |
| 319 | Env: env, | 394 | Env: opts.Env, |
| 320 | Args: args, | 395 | Args: opts.Args, |
| 321 | Files: files, | 396 | Files: opts.Files, |
| 322 | Memory: memory, | 397 | Memory: opts.Memory, |
| 323 | CPU: cpu, | 398 | CPU: opts.CPU, |
| 324 | }) | 399 | }) |
| 325 | if err := st.Save(); err != nil { | 400 | if err := st.Save(); err != nil { |
| 326 | return fmt.Errorf("error saving state: %w", err) | 401 | return fmt.Errorf("error saving state: %w", err) |
| @@ -334,12 +409,7 @@ func deployApp(opts DeployOptions) error { | |||
| 334 | return nil | 409 | return nil |
| 335 | } | 410 | } |
| 336 | 411 | ||
| 337 | func updateAppConfig(opts DeployOptions) error { | 412 | func updateAppConfig(st *state.State, opts DeployOptions) error { |
| 338 | st, err := state.Load() | ||
| 339 | if err != nil { | ||
| 340 | return fmt.Errorf("error loading state: %w", err) | ||
| 341 | } | ||
| 342 | |||
| 343 | existingApp, err := st.GetApp(opts.Host, opts.Name) | 413 | existingApp, err := st.GetApp(opts.Host, opts.Name) |
| 344 | if err != nil { | 414 | if err != nil { |
| 345 | return fmt.Errorf("app %s not found (use --binary to deploy a new app)", opts.Name) | 415 | return fmt.Errorf("app %s not found (use --binary to deploy a new app)", opts.Name) |
| @@ -351,41 +421,8 @@ func updateAppConfig(opts DeployOptions) error { | |||
| 351 | 421 | ||
| 352 | fmt.Printf("Updating config: %s\n", opts.Name) | 422 | fmt.Printf("Updating config: %s\n", opts.Name) |
| 353 | 423 | ||
| 354 | // Merge with existing values | 424 | // Add PORT to env |
| 355 | args := opts.Args | 425 | opts.Env["PORT"] = strconv.Itoa(existingApp.Port) |
| 356 | if args == "" { | ||
| 357 | args = existingApp.Args | ||
| 358 | } | ||
| 359 | memory := opts.Memory | ||
| 360 | if memory == "" { | ||
| 361 | memory = existingApp.Memory | ||
| 362 | } | ||
| 363 | cpu := opts.CPU | ||
| 364 | if cpu == "" { | ||
| 365 | cpu = existingApp.CPU | ||
| 366 | } | ||
| 367 | |||
| 368 | // Merge env vars | ||
| 369 | env := make(map[string]string) | ||
| 370 | for k, v := range existingApp.Env { | ||
| 371 | env[k] = v | ||
| 372 | } | ||
| 373 | for _, e := range opts.EnvVars { | ||
| 374 | parts := strings.SplitN(e, "=", 2) | ||
| 375 | if len(parts) == 2 { | ||
| 376 | env[parts[0]] = parts[1] | ||
| 377 | } | ||
| 378 | } | ||
| 379 | if opts.EnvFile != "" { | ||
| 380 | fileEnv, err := parseEnvFile(opts.EnvFile) | ||
| 381 | if err != nil { | ||
| 382 | return fmt.Errorf("error reading env file: %w", err) | ||
| 383 | } | ||
| 384 | for k, v := range fileEnv { | ||
| 385 | env[k] = v | ||
| 386 | } | ||
| 387 | } | ||
| 388 | env["PORT"] = strconv.Itoa(existingApp.Port) | ||
| 389 | 426 | ||
| 390 | client, err := ssh.Connect(opts.Host) | 427 | client, err := ssh.Connect(opts.Host) |
| 391 | if err != nil { | 428 | if err != nil { |
| @@ -397,7 +434,7 @@ func updateAppConfig(opts DeployOptions) error { | |||
| 397 | fmt.Println("-> Updating environment file...") | 434 | fmt.Println("-> Updating environment file...") |
| 398 | envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", opts.Name) | 435 | envFilePath := fmt.Sprintf("/etc/ship/env/%s.env", opts.Name) |
| 399 | envContent := "" | 436 | envContent := "" |
| 400 | for k, v := range env { | 437 | for k, v := range opts.Env { |
| 401 | envContent += fmt.Sprintf("%s=%s\n", k, v) | 438 | envContent += fmt.Sprintf("%s=%s\n", k, v) |
| 402 | } | 439 | } |
| 403 | if err := client.WriteSudoFile(envFilePath, envContent); err != nil { | 440 | if err := client.WriteSudoFile(envFilePath, envContent); err != nil { |
| @@ -415,9 +452,9 @@ func updateAppConfig(opts DeployOptions) error { | |||
| 415 | "BinaryPath": binaryDest, | 452 | "BinaryPath": binaryDest, |
| 416 | "Port": strconv.Itoa(existingApp.Port), | 453 | "Port": strconv.Itoa(existingApp.Port), |
| 417 | "EnvFile": envFilePath, | 454 | "EnvFile": envFilePath, |
| 418 | "Args": args, | 455 | "Args": opts.Args, |
| 419 | "Memory": memory, | 456 | "Memory": opts.Memory, |
| 420 | "CPU": cpu, | 457 | "CPU": opts.CPU, |
| 421 | }) | 458 | }) |
| 422 | if err != nil { | 459 | if err != nil { |
| 423 | return fmt.Errorf("error generating systemd unit: %w", err) | 460 | return fmt.Errorf("error generating systemd unit: %w", err) |
| @@ -439,10 +476,10 @@ func updateAppConfig(opts DeployOptions) error { | |||
| 439 | } | 476 | } |
| 440 | 477 | ||
| 441 | // Update state | 478 | // Update state |
| 442 | existingApp.Args = args | 479 | existingApp.Args = opts.Args |
| 443 | existingApp.Memory = memory | 480 | existingApp.Memory = opts.Memory |
| 444 | existingApp.CPU = cpu | 481 | existingApp.CPU = opts.CPU |
| 445 | existingApp.Env = env | 482 | existingApp.Env = opts.Env |
| 446 | if err := st.Save(); err != nil { | 483 | if err := st.Save(); err != nil { |
| 447 | return fmt.Errorf("error saving state: %w", err) | 484 | return fmt.Errorf("error saving state: %w", err) |
| 448 | } | 485 | } |
| @@ -451,27 +488,26 @@ func updateAppConfig(opts DeployOptions) error { | |||
| 451 | return nil | 488 | return nil |
| 452 | } | 489 | } |
| 453 | 490 | ||
| 454 | func deployStatic(host, domain, name, dir string) error { | 491 | func deployStatic(st *state.State, opts DeployOptions) error { |
| 455 | if _, err := os.Stat(dir); err != nil { | 492 | if _, err := os.Stat(opts.Dir); err != nil { |
| 456 | return fmt.Errorf("directory not found: %s", dir) | 493 | return fmt.Errorf("directory not found: %s", opts.Dir) |
| 457 | } | 494 | } |
| 458 | 495 | ||
| 459 | fmt.Printf("Deploying static site: %s\n", name) | 496 | fmt.Printf("Deploying static site: %s\n", opts.Name) |
| 460 | fmt.Printf(" Domain(s): %s\n", domain) | 497 | fmt.Printf(" Domain(s): %s\n", opts.Domain) |
| 461 | fmt.Printf(" Directory: %s\n", dir) | 498 | fmt.Printf(" Directory: %s\n", opts.Dir) |
| 462 | 499 | ||
| 463 | st, err := state.Load() | 500 | if opts.IsUpdate { |
| 464 | if err != nil { | 501 | fmt.Println(" Updating existing deployment") |
| 465 | return fmt.Errorf("error loading state: %w", err) | ||
| 466 | } | 502 | } |
| 467 | 503 | ||
| 468 | client, err := ssh.Connect(host) | 504 | client, err := ssh.Connect(opts.Host) |
| 469 | if err != nil { | 505 | if err != nil { |
| 470 | return fmt.Errorf("error connecting to VPS: %w", err) | 506 | return fmt.Errorf("error connecting to VPS: %w", err) |
| 471 | } | 507 | } |
| 472 | defer client.Close() | 508 | defer client.Close() |
| 473 | 509 | ||
| 474 | remoteDir := fmt.Sprintf("/var/www/%s", name) | 510 | remoteDir := fmt.Sprintf("/var/www/%s", opts.Name) |
| 475 | fmt.Println("-> Creating remote directory...") | 511 | fmt.Println("-> Creating remote directory...") |
| 476 | if _, err := client.RunSudo(fmt.Sprintf("mkdir -p %s", remoteDir)); err != nil { | 512 | if _, err := client.RunSudo(fmt.Sprintf("mkdir -p %s", remoteDir)); err != nil { |
| 477 | return fmt.Errorf("error creating remote directory: %w", err) | 513 | return fmt.Errorf("error creating remote directory: %w", err) |
| @@ -488,7 +524,7 @@ func deployStatic(host, domain, name, dir string) error { | |||
| 488 | } | 524 | } |
| 489 | 525 | ||
| 490 | fmt.Println("-> Uploading files...") | 526 | fmt.Println("-> Uploading files...") |
| 491 | if err := client.UploadDir(dir, remoteDir); err != nil { | 527 | if err := client.UploadDir(opts.Dir, remoteDir); err != nil { |
| 492 | return fmt.Errorf("error uploading files: %w", err) | 528 | return fmt.Errorf("error uploading files: %w", err) |
| 493 | } | 529 | } |
| 494 | 530 | ||
| @@ -505,14 +541,14 @@ func deployStatic(host, domain, name, dir string) error { | |||
| 505 | 541 | ||
| 506 | fmt.Println("-> Configuring Caddy...") | 542 | fmt.Println("-> Configuring Caddy...") |
| 507 | caddyContent, err := templates.StaticCaddy(map[string]string{ | 543 | caddyContent, err := templates.StaticCaddy(map[string]string{ |
| 508 | "Domain": domain, | 544 | "Domain": opts.Domain, |
| 509 | "RootDir": remoteDir, | 545 | "RootDir": remoteDir, |
| 510 | }) | 546 | }) |
| 511 | if err != nil { | 547 | if err != nil { |
| 512 | return fmt.Errorf("error generating Caddy config: %w", err) | 548 | return fmt.Errorf("error generating Caddy config: %w", err) |
| 513 | } | 549 | } |
| 514 | 550 | ||
| 515 | caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) | 551 | caddyPath := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", opts.Name) |
| 516 | if err := client.WriteSudoFile(caddyPath, caddyContent); err != nil { | 552 | if err := client.WriteSudoFile(caddyPath, caddyContent); err != nil { |
| 517 | return fmt.Errorf("error creating Caddy config: %w", err) | 553 | return fmt.Errorf("error creating Caddy config: %w", err) |
| 518 | } | 554 | } |
| @@ -522,17 +558,16 @@ func deployStatic(host, domain, name, dir string) error { | |||
| 522 | return fmt.Errorf("error reloading Caddy: %w", err) | 558 | return fmt.Errorf("error reloading Caddy: %w", err) |
| 523 | } | 559 | } |
| 524 | 560 | ||
| 525 | st.AddApp(host, name, &state.App{ | 561 | st.AddApp(opts.Host, opts.Name, &state.App{ |
| 526 | Type: "static", | 562 | Type: "static", |
| 527 | Domain: domain, | 563 | Domain: opts.Domain, |
| 528 | }) | 564 | }) |
| 529 | if err := st.Save(); err != nil { | 565 | if err := st.Save(); err != nil { |
| 530 | return fmt.Errorf("error saving state: %w", err) | 566 | return fmt.Errorf("error saving state: %w", err) |
| 531 | } | 567 | } |
| 532 | 568 | ||
| 533 | fmt.Printf("\n Static site deployed successfully!\n") | 569 | fmt.Printf("\n Static site deployed successfully!\n") |
| 534 | // Show first domain in the URL message | 570 | primaryDomain := strings.Split(opts.Domain, ",")[0] |
| 535 | primaryDomain := strings.Split(domain, ",")[0] | ||
| 536 | primaryDomain = strings.TrimSpace(primaryDomain) | 571 | primaryDomain = strings.TrimSpace(primaryDomain) |
| 537 | fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", primaryDomain) | 572 | fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", primaryDomain) |
| 538 | return nil | 573 | return nil |
