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 := result.ContainerID
75 if containerID != "" && len(containerID) > 12 {
76 containerID = containerID[:12]
77 }
78
79 switch result.Status {
80 case "deployed":
81 msg := fmt.Sprintf("Deployed %s", messageDomain)
82 if containerID != "" {
83 msg += fmt.Sprintf(" (container: %s)", containerID)
84 }
85 return cliWriteLine(out, cliRenderSuccess(msg))
86 case "queued":
87 return cliWriteLine(out, cliRenderSuccess(fmt.Sprintf("Deploy queued for domain: %s", messageDomain)))
88 default:
89 msg := fmt.Sprintf("Deploy status for %s: %s", messageDomain, result.Status)
90 if containerID != "" {
91 msg += fmt.Sprintf(" (container: %s)", containerID)
92 }
93 return cliWriteLine(out, cliRenderInfo(msg))
94 }
95}
96
97func handleLocalDeployFallback(err error, deployDomain string, out io.Writer, jsonOut bool) error {
98 domain, localErr := sendDeploySignal(deployDomain)
99 if localErr != nil {
100 return fmt.Errorf("failed to deploy: remote error: %w; local fallback also failed: %w", err, localErr)
101 }
102
103 warning := sanitizeDeployLogLine(fmt.Sprintf("Remote deploy failed (%v), used local signal fallback", err))
104 if jsonOut {
105 return writeJSON(out, map[string]string{
106 "warning": warning,
107 "domain": domain,
108 "status": "success",
109 })
110 }
111
112 if writeErr := cliWriteLine(out, cliRenderWarning(warning)); writeErr != nil {
113 return writeErr
114 }
115
116 return cliWriteLine(out, cliRenderSuccess(fmt.Sprintf("Deploy signal sent for domain: %s", domain)))
117}