summaryrefslogtreecommitdiffstats
path: root/cmd/ship
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/ship')
-rw-r--r--cmd/ship/root.go395
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
18type DeployOptions struct { 18type 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
32func runDeploy(cmd *cobra.Command, args []string) error { 33func 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
112func deployApp(opts DeployOptions) error { 235func 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
337func updateAppConfig(opts DeployOptions) error { 412func 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
454func deployStatic(host, domain, name, dir string) error { 491func 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