1package cli
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "sort"
8 "strings"
9
10 "github.com/spf13/cobra"
11
12 "github.com/bnema/gordon/internal/adapters/dto"
13)
14
15func newBootstrapCmd() *cobra.Command {
16 var attachments []string
17 var envPairs []string
18 var attachmentEnvPairs []string
19 var configPath string
20
21 cmd := &cobra.Command{
22 Use: "bootstrap <domain> <image>",
23 Short: "Create a route, attachments, and secrets together",
24 Args: cobra.ExactArgs(2),
25 RunE: func(cmd *cobra.Command, args []string) error {
26 req, err := parseBootstrapRequest(args, attachments, envPairs, attachmentEnvPairs)
27 if err != nil {
28 return err
29 }
30 return runBootstrap(cmd.Context(), req, configPath, cmd.OutOrStdout())
31 },
32 }
33
34 cmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to config file")
35 cmd.Flags().StringArrayVar(&attachments, "attachment", nil, "Attachment image (repeatable)")
36 cmd.Flags().StringArrayVar(&envPairs, "env", nil, "Environment variable KEY=VALUE (repeatable)")
37 cmd.Flags().StringArrayVar(&attachmentEnvPairs, "attachment-env", nil, "Attachment env service:KEY=VALUE (repeatable)")
38
39 return cmd
40}
41
42func parseBootstrapRequest(args []string, attachments, envPairs, attachmentEnvPairs []string) (dto.BootstrapRequest, error) {
43 req := dto.BootstrapRequest{
44 Domain: args[0],
45 Image: args[1],
46 Attachments: append([]string(nil), attachments...),
47 Env: map[string]string{},
48 AttachmentEnv: map[string]map[string]string{},
49 }
50
51 for _, pair := range envPairs {
52 parts := strings.SplitN(pair, "=", 2)
53 if len(parts) != 2 {
54 return dto.BootstrapRequest{}, fmt.Errorf("invalid --env value %q (expected KEY=VALUE)", pair)
55 }
56 req.Env[parts[0]] = parts[1]
57 }
58
59 for _, pair := range attachmentEnvPairs {
60 serviceAndKV := strings.SplitN(pair, ":", 2)
61 if len(serviceAndKV) != 2 {
62 return dto.BootstrapRequest{}, fmt.Errorf("invalid --attachment-env value %q (expected service:KEY=VALUE)", pair)
63 }
64 keyValue := strings.SplitN(serviceAndKV[1], "=", 2)
65 if len(keyValue) != 2 {
66 return dto.BootstrapRequest{}, fmt.Errorf("invalid --attachment-env value %q (expected service:KEY=VALUE)", pair)
67 }
68 service := serviceAndKV[0]
69 if req.AttachmentEnv[service] == nil {
70 req.AttachmentEnv[service] = map[string]string{}
71 }
72 req.AttachmentEnv[service][keyValue[0]] = keyValue[1]
73 }
74
75 if len(req.Env) == 0 {
76 req.Env = nil
77 }
78 if len(req.AttachmentEnv) == 0 {
79 req.AttachmentEnv = nil
80 }
81
82 return req, nil
83}
84
85func runBootstrap(ctx context.Context, req dto.BootstrapRequest, configPath string, out io.Writer) error {
86 handle, err := resolveControlPlane(configPath)
87 if err != nil {
88 return err
89 }
90 defer handle.close()
91
92 resp, err := handle.plane.Bootstrap(ctx, req)
93 if resp != nil {
94 if writeErr := printBootstrapSummary(out, resp); writeErr != nil {
95 return writeErr
96 }
97 }
98 if err != nil {
99 return fmt.Errorf("failed to bootstrap: %w", err)
100 }
101 if err := cliWriteLine(out, cliRenderSuccess("Bootstrap complete")); err != nil {
102 return err
103 }
104 nextImage := req.Image
105 if resp != nil && resp.Image != "" {
106 nextImage = resp.Image
107 }
108 return cliWriteLine(out, fmt.Sprintf("Next: gordon push %s --build", nextImage))
109}
110
111func printBootstrapSummary(out io.Writer, resp *dto.BootstrapResponse) error {
112 if err := cliWriteLine(out, cliRenderTitle("Bootstrap")); err != nil {
113 return err
114 }
115 if err := cliWriteLine(out, cliRenderMeta("Domain:", resp.Domain)); err != nil {
116 return err
117 }
118 if err := cliWriteLine(out, cliRenderMeta("Image:", resp.Image)); err != nil {
119 return err
120 }
121 for _, step := range resp.Steps {
122 if err := cliWriteLine(out, fmt.Sprintf("- %s: %s", step.Name, step.Status)); err != nil {
123 return err
124 }
125 }
126 if len(resp.Warnings) > 0 {
127 sorted := append([]string(nil), resp.Warnings...)
128 sort.Strings(sorted)
129 for _, warning := range sorted {
130 if err := cliWriteLine(out, cliRenderWarning(warning)); err != nil {
131 return err
132 }
133 }
134 }
135 return nil
136}