1package cli
2
3import (
4 "context"
5 "fmt"
6 "io"
7
8 "github.com/spf13/cobra"
9
10 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
11 "github.com/bnema/gordon/internal/app"
12)
13
14type deployer interface {
15 Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error)
16}
17
18var sendDeploySignal = app.SendDeploySignal
19
20// newDeployCmd creates the deploy command.
21func newDeployCmd() *cobra.Command {
22 var jsonOut bool
23 cmd := &cobra.Command{
24 Use: "deploy <domain>",
25 Short: "Manually deploy or redeploy a route",
26 Long: `Triggers a deployment for the specified route domain.
27The route must be configured in config.toml.
28
29This will pull the latest image and deploy/redeploy the container,
30even if a container is already running.
31
32Examples:
33 gordon deploy myapp.example.com
34 gordon deploy api.example.com
35 gordon deploy myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN`,
36 Args: cobra.ExactArgs(1),
37 RunE: func(cmd *cobra.Command, args []string) error {
38 handle, err := resolveControlPlane(configPath)
39 if err != nil {
40 return err
41 }
42 defer handle.close()
43
44 return runDeploy(cmd.Context(), handle.plane, handle.isRemote, args[0], cmd.OutOrStdout(), jsonOut)
45 },
46 }
47 cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON")
48 return cmd
49}
50
51func runDeploy(ctx context.Context, deployer deployer, isRemote bool, deployDomain string, out io.Writer, jsonOut bool) error {
52 result, err := deployer.Deploy(ctx, deployDomain)
53 if err != nil {
54 if formatted, ok := structuredDeployFailure(err); ok {
55 return formatted
56 }
57
58 if isRemote && shouldFallbackToLocal(err) {
59 return handleLocalDeployFallback(err, deployDomain, out, jsonOut)
60 }
61
62 return formatDeployFailure(err)
63 }
64
65 if jsonOut {
66 return writeJSON(out, result)
67 }
68
69 messageDomain := deployDomain
70 if result.Domain != "" {
71 messageDomain = result.Domain
72 }
73
74 containerID := shortContainerID(result.ContainerID)
75
76 switch result.Status {
77 case "deployed":
78 msg := fmt.Sprintf("Deployed %s", messageDomain)
79 if containerID != "" {
80 msg += fmt.Sprintf(" (container: %s)", containerID)
81 }
82 return cliWriteLine(out, cliRenderSuccess(msg))
83 case "queued":
84 return cliWriteLine(out, cliRenderSuccess(fmt.Sprintf("Deploy queued for domain: %s", messageDomain)))
85 default:
86 msg := fmt.Sprintf("Deploy status for %s: %s", messageDomain, result.Status)
87 if containerID != "" {
88 msg += fmt.Sprintf(" (container: %s)", containerID)
89 }
90 return cliWriteLine(out, cliRenderInfo(msg))
91 }
92}
93
94func handleLocalDeployFallback(err error, deployDomain string, out io.Writer, jsonOut bool) error {
95 domain, localErr := sendDeploySignal(deployDomain)
96 if localErr != nil {
97 return fmt.Errorf("failed to deploy: remote error: %w; local fallback also failed: %w", err, localErr)
98 }
99
100 warning := sanitizeDeployLogLine(fmt.Sprintf("Remote deploy failed (%v), used local signal fallback", err))
101 if jsonOut {
102 return writeJSON(out, map[string]string{
103 "warning": warning,
104 "domain": domain,
105 "status": "success",
106 })
107 }
108
109 if writeErr := cliWriteLine(out, cliRenderWarning(warning)); writeErr != nil {
110 return writeErr
111 }
112
113 return cliWriteLine(out, cliRenderSuccess(fmt.Sprintf("Deploy signal sent for domain: %s", domain)))
114}