diff options
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/ship/host/host.go | 3 | ||||
| -rw-r--r-- | cmd/ship/host/init.go | 8 | ||||
| -rw-r--r-- | cmd/ship/host/set_domain.go | 76 | ||||
| -rw-r--r-- | cmd/ship/list.go | 6 | ||||
| -rw-r--r-- | cmd/ship/main.go | 4 | ||||
| -rw-r--r-- | cmd/ship/root.go | 63 |
6 files changed, 141 insertions, 19 deletions
diff --git a/cmd/ship/host/host.go b/cmd/ship/host/host.go index 603a946..81403f9 100644 --- a/cmd/ship/host/host.go +++ b/cmd/ship/host/host.go | |||
| @@ -15,4 +15,7 @@ func init() { | |||
| 15 | Cmd.AddCommand(statusCmd) | 15 | Cmd.AddCommand(statusCmd) |
| 16 | Cmd.AddCommand(updateCmd) | 16 | Cmd.AddCommand(updateCmd) |
| 17 | Cmd.AddCommand(sshCmd) | 17 | Cmd.AddCommand(sshCmd) |
| 18 | Cmd.AddCommand(setDomainCmd) | ||
| 19 | |||
| 20 | initCmd.Flags().String("base-domain", "", "Base domain for auto-generated subdomains (e.g., apps.example.com)") | ||
| 18 | } | 21 | } |
diff --git a/cmd/ship/host/init.go b/cmd/ship/host/init.go index ea25922..27f67af 100644 --- a/cmd/ship/host/init.go +++ b/cmd/ship/host/init.go | |||
| @@ -26,6 +26,7 @@ func runInit(cmd *cobra.Command, args []string) error { | |||
| 26 | if host == "" { | 26 | if host == "" { |
| 27 | host = st.GetDefaultHost() | 27 | host = st.GetDefaultHost() |
| 28 | } | 28 | } |
| 29 | baseDomain, _ := cmd.Flags().GetString("base-domain") | ||
| 29 | 30 | ||
| 30 | if host == "" { | 31 | if host == "" { |
| 31 | return fmt.Errorf("--host is required") | 32 | return fmt.Errorf("--host is required") |
| @@ -64,7 +65,6 @@ func runInit(cmd *cobra.Command, args []string) error { | |||
| 64 | 65 | ||
| 65 | fmt.Println("-> Configuring Caddy...") | 66 | fmt.Println("-> Configuring Caddy...") |
| 66 | caddyfile := `{ | 67 | caddyfile := `{ |
| 67 | email admin@example.com | ||
| 68 | } | 68 | } |
| 69 | 69 | ||
| 70 | import /etc/caddy/sites-enabled/* | 70 | import /etc/caddy/sites-enabled/* |
| @@ -100,7 +100,11 @@ import /etc/caddy/sites-enabled/* | |||
| 100 | fmt.Println(" Caddy is active") | 100 | fmt.Println(" Caddy is active") |
| 101 | } | 101 | } |
| 102 | 102 | ||
| 103 | st.GetHost(host) | 103 | hostState := st.GetHost(host) |
| 104 | if baseDomain != "" { | ||
| 105 | hostState.BaseDomain = baseDomain | ||
| 106 | fmt.Printf(" Base domain: %s\n", baseDomain) | ||
| 107 | } | ||
| 104 | if st.GetDefaultHost() == "" { | 108 | if st.GetDefaultHost() == "" { |
| 105 | st.SetDefaultHost(host) | 109 | st.SetDefaultHost(host) |
| 106 | fmt.Printf(" Set %s as default host\n", host) | 110 | fmt.Printf(" Set %s as default host\n", host) |
diff --git a/cmd/ship/host/set_domain.go b/cmd/ship/host/set_domain.go new file mode 100644 index 0000000..fed3b31 --- /dev/null +++ b/cmd/ship/host/set_domain.go | |||
| @@ -0,0 +1,76 @@ | |||
| 1 | package host | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | |||
| 6 | "github.com/bdw/ship/internal/state" | ||
| 7 | "github.com/spf13/cobra" | ||
| 8 | ) | ||
| 9 | |||
| 10 | var setDomainCmd = &cobra.Command{ | ||
| 11 | Use: "set-domain [domain]", | ||
| 12 | Short: "Set base domain for auto-generated subdomains", | ||
| 13 | Long: `Set the base domain used to auto-generate subdomains for deployments. | ||
| 14 | |||
| 15 | When a base domain is configured (e.g., apps.example.com), every deployment | ||
| 16 | will automatically get a subdomain ({name}.apps.example.com). | ||
| 17 | |||
| 18 | Examples: | ||
| 19 | ship host set-domain apps.example.com # Set base domain | ||
| 20 | ship host set-domain --clear # Remove base domain`, | ||
| 21 | RunE: runSetDomain, | ||
| 22 | } | ||
| 23 | |||
| 24 | func init() { | ||
| 25 | setDomainCmd.Flags().Bool("clear", false, "Clear the base domain") | ||
| 26 | } | ||
| 27 | |||
| 28 | func runSetDomain(cmd *cobra.Command, args []string) error { | ||
| 29 | st, err := state.Load() | ||
| 30 | if err != nil { | ||
| 31 | return fmt.Errorf("error loading state: %w", err) | ||
| 32 | } | ||
| 33 | |||
| 34 | host, _ := cmd.Flags().GetString("host") | ||
| 35 | if host == "" { | ||
| 36 | host = st.GetDefaultHost() | ||
| 37 | } | ||
| 38 | |||
| 39 | if host == "" { | ||
| 40 | return fmt.Errorf("--host is required") | ||
| 41 | } | ||
| 42 | |||
| 43 | clear, _ := cmd.Flags().GetBool("clear") | ||
| 44 | |||
| 45 | if !clear && len(args) == 0 { | ||
| 46 | // Show current base domain | ||
| 47 | hostState := st.GetHost(host) | ||
| 48 | if hostState.BaseDomain == "" { | ||
| 49 | fmt.Printf("No base domain configured for %s\n", host) | ||
| 50 | } else { | ||
| 51 | fmt.Printf("Base domain for %s: %s\n", host, hostState.BaseDomain) | ||
| 52 | } | ||
| 53 | return nil | ||
| 54 | } | ||
| 55 | |||
| 56 | hostState := st.GetHost(host) | ||
| 57 | |||
| 58 | if clear { | ||
| 59 | hostState.BaseDomain = "" | ||
| 60 | if err := st.Save(); err != nil { | ||
| 61 | return fmt.Errorf("error saving state: %w", err) | ||
| 62 | } | ||
| 63 | fmt.Printf("Cleared base domain for %s\n", host) | ||
| 64 | return nil | ||
| 65 | } | ||
| 66 | |||
| 67 | hostState.BaseDomain = args[0] | ||
| 68 | if err := st.Save(); err != nil { | ||
| 69 | return fmt.Errorf("error saving state: %w", err) | ||
| 70 | } | ||
| 71 | |||
| 72 | fmt.Printf("Set base domain for %s: %s\n", host, args[0]) | ||
| 73 | fmt.Println("\nNew deployments will automatically use subdomains like:") | ||
| 74 | fmt.Printf(" myapp.%s\n", args[0]) | ||
| 75 | return nil | ||
| 76 | } | ||
diff --git a/cmd/ship/list.go b/cmd/ship/list.go index a5b8df3..404ca68 100644 --- a/cmd/ship/list.go +++ b/cmd/ship/list.go | |||
| @@ -43,7 +43,11 @@ func runList(cmd *cobra.Command, args []string) error { | |||
| 43 | if app.Type == "app" { | 43 | if app.Type == "app" { |
| 44 | port = fmt.Sprintf(":%d", app.Port) | 44 | port = fmt.Sprintf(":%d", app.Port) |
| 45 | } | 45 | } |
| 46 | fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, app.Type, app.Domain, port) | 46 | domain := app.Domain |
| 47 | if domain == "" { | ||
| 48 | domain = "-" | ||
| 49 | } | ||
| 50 | fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, app.Type, domain, port) | ||
| 47 | } | 51 | } |
| 48 | w.Flush() | 52 | w.Flush() |
| 49 | return nil | 53 | return nil |
diff --git a/cmd/ship/main.go b/cmd/ship/main.go index 27fe8b7..a6984ec 100644 --- a/cmd/ship/main.go +++ b/cmd/ship/main.go | |||
| @@ -1,6 +1,7 @@ | |||
| 1 | package main | 1 | package main |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "fmt" | ||
| 4 | "os" | 5 | "os" |
| 5 | 6 | ||
| 6 | "github.com/bdw/ship/cmd/ship/env" | 7 | "github.com/bdw/ship/cmd/ship/env" |
| @@ -57,7 +58,7 @@ func init() { | |||
| 57 | rootCmd.Flags().String("binary", "", "Path to Go binary (for app deployment)") | 58 | rootCmd.Flags().String("binary", "", "Path to Go binary (for app deployment)") |
| 58 | rootCmd.Flags().Bool("static", false, "Deploy as static site") | 59 | rootCmd.Flags().Bool("static", false, "Deploy as static site") |
| 59 | rootCmd.Flags().String("dir", ".", "Directory to deploy (for static sites)") | 60 | rootCmd.Flags().String("dir", ".", "Directory to deploy (for static sites)") |
| 60 | rootCmd.Flags().String("domain", "", "Domain name (required)") | 61 | rootCmd.Flags().String("domain", "", "Custom domain (optional if base domain configured)") |
| 61 | rootCmd.Flags().String("name", "", "App name (default: inferred from binary or directory)") | 62 | rootCmd.Flags().String("name", "", "App name (default: inferred from binary or directory)") |
| 62 | rootCmd.Flags().Int("port", 0, "Port override (default: auto-allocate)") | 63 | rootCmd.Flags().Int("port", 0, "Port override (default: auto-allocate)") |
| 63 | rootCmd.Flags().StringArray("env", nil, "Environment variable (KEY=VALUE, can be specified multiple times)") | 64 | rootCmd.Flags().StringArray("env", nil, "Environment variable (KEY=VALUE, can be specified multiple times)") |
| @@ -79,6 +80,7 @@ func init() { | |||
| 79 | 80 | ||
| 80 | func main() { | 81 | func main() { |
| 81 | if err := rootCmd.Execute(); err != nil { | 82 | if err := rootCmd.Execute(); err != nil { |
| 83 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) | ||
| 82 | os.Exit(1) | 84 | os.Exit(1) |
| 83 | } | 85 | } |
| 84 | } | 86 | } |
diff --git a/cmd/ship/root.go b/cmd/ship/root.go index e5d6753..81d33d1 100644 --- a/cmd/ship/root.go +++ b/cmd/ship/root.go | |||
| @@ -43,14 +43,48 @@ func runDeploy(cmd *cobra.Command, args []string) error { | |||
| 43 | return cmd.Help() | 43 | return cmd.Help() |
| 44 | } | 44 | } |
| 45 | 45 | ||
| 46 | if host == "" || domain == "" { | 46 | if host == "" { |
| 47 | return fmt.Errorf("--host and --domain are required") | 47 | return fmt.Errorf("--host is required") |
| 48 | } | ||
| 49 | |||
| 50 | // Load state to check base domain | ||
| 51 | st, err := state.Load() | ||
| 52 | if err != nil { | ||
| 53 | return fmt.Errorf("error loading state: %w", err) | ||
| 54 | } | ||
| 55 | hostState := st.GetHost(host) | ||
| 56 | |||
| 57 | if domain == "" && hostState.BaseDomain == "" { | ||
| 58 | return fmt.Errorf("--domain required (or configure base domain with 'ship host init --base-domain')") | ||
| 48 | } | 59 | } |
| 49 | 60 | ||
| 61 | // Infer name early so we can use it for subdomain generation | ||
| 62 | inferredName := name | ||
| 63 | if inferredName == "" { | ||
| 64 | if static { | ||
| 65 | inferredName = domain | ||
| 66 | if inferredName == "" && hostState.BaseDomain != "" { | ||
| 67 | inferredName = filepath.Base(dir) | ||
| 68 | } | ||
| 69 | } else { | ||
| 70 | inferredName = filepath.Base(binary) | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | // Generate subdomain if base domain configured | ||
| 75 | var domains []string | ||
| 76 | if hostState.BaseDomain != "" { | ||
| 77 | domains = append(domains, inferredName+"."+hostState.BaseDomain) | ||
| 78 | } | ||
| 79 | if domain != "" { | ||
| 80 | domains = append(domains, domain) | ||
| 81 | } | ||
| 82 | combinedDomains := strings.Join(domains, ", ") | ||
| 83 | |||
| 50 | if static { | 84 | if static { |
| 51 | return deployStatic(host, domain, name, dir) | 85 | return deployStatic(host, combinedDomains, inferredName, dir) |
| 52 | } | 86 | } |
| 53 | return deployApp(host, domain, name, binary, port, envVars, envFile, binaryArgs, files) | 87 | return deployApp(host, combinedDomains, inferredName, binary, port, envVars, envFile, binaryArgs, files) |
| 54 | } | 88 | } |
| 55 | 89 | ||
| 56 | func deployApp(host, domain, name, binaryPath string, portOverride int, envVars []string, envFile, args string, files []string) error { | 90 | func deployApp(host, domain, name, binaryPath string, portOverride int, envVars []string, envFile, args string, files []string) error { |
| @@ -58,16 +92,12 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars | |||
| 58 | return fmt.Errorf("--binary is required") | 92 | return fmt.Errorf("--binary is required") |
| 59 | } | 93 | } |
| 60 | 94 | ||
| 61 | if name == "" { | ||
| 62 | name = filepath.Base(binaryPath) | ||
| 63 | } | ||
| 64 | |||
| 65 | if _, err := os.Stat(binaryPath); err != nil { | 95 | if _, err := os.Stat(binaryPath); err != nil { |
| 66 | return fmt.Errorf("binary not found: %s", binaryPath) | 96 | return fmt.Errorf("binary not found: %s", binaryPath) |
| 67 | } | 97 | } |
| 68 | 98 | ||
| 69 | fmt.Printf("Deploying app: %s\n", name) | 99 | fmt.Printf("Deploying app: %s\n", name) |
| 70 | fmt.Printf(" Domain: %s\n", domain) | 100 | fmt.Printf(" Domain(s): %s\n", domain) |
| 71 | fmt.Printf(" Binary: %s\n", binaryPath) | 101 | fmt.Printf(" Binary: %s\n", binaryPath) |
| 72 | 102 | ||
| 73 | st, err := state.Load() | 103 | st, err := state.Load() |
| @@ -260,21 +290,21 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars | |||
| 260 | } | 290 | } |
| 261 | 291 | ||
| 262 | fmt.Printf("\n App deployed successfully!\n") | 292 | fmt.Printf("\n App deployed successfully!\n") |
| 263 | fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", domain) | 293 | // Show first domain in the URL message |
| 294 | primaryDomain := strings.Split(domain, ",")[0] | ||
| 295 | primaryDomain = strings.TrimSpace(primaryDomain) | ||
| 296 | fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", primaryDomain) | ||
| 264 | return nil | 297 | return nil |
| 265 | } | 298 | } |
| 266 | 299 | ||
| 267 | func deployStatic(host, domain, name, dir string) error { | 300 | func deployStatic(host, domain, name, dir string) error { |
| 268 | if name == "" { | ||
| 269 | name = domain | ||
| 270 | } | ||
| 271 | 301 | ||
| 272 | if _, err := os.Stat(dir); err != nil { | 302 | if _, err := os.Stat(dir); err != nil { |
| 273 | return fmt.Errorf("directory not found: %s", dir) | 303 | return fmt.Errorf("directory not found: %s", dir) |
| 274 | } | 304 | } |
| 275 | 305 | ||
| 276 | fmt.Printf("Deploying static site: %s\n", name) | 306 | fmt.Printf("Deploying static site: %s\n", name) |
| 277 | fmt.Printf(" Domain: %s\n", domain) | 307 | fmt.Printf(" Domain(s): %s\n", domain) |
| 278 | fmt.Printf(" Directory: %s\n", dir) | 308 | fmt.Printf(" Directory: %s\n", dir) |
| 279 | 309 | ||
| 280 | st, err := state.Load() | 310 | st, err := state.Load() |
| @@ -348,7 +378,10 @@ func deployStatic(host, domain, name, dir string) error { | |||
| 348 | } | 378 | } |
| 349 | 379 | ||
| 350 | fmt.Printf("\n Static site deployed successfully!\n") | 380 | fmt.Printf("\n Static site deployed successfully!\n") |
| 351 | fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", domain) | 381 | // Show first domain in the URL message |
| 382 | primaryDomain := strings.Split(domain, ",")[0] | ||
| 383 | primaryDomain = strings.TrimSpace(primaryDomain) | ||
| 384 | fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", primaryDomain) | ||
| 352 | return nil | 385 | return nil |
| 353 | } | 386 | } |
| 354 | 387 | ||
