1// gordon/internal/cli/cmd/deploy.go
2
3package cmd
4
5import (
6 "encoding/json"
7 "fmt"
8 "io"
9 "net/http"
10 "os"
11 "strings"
12 "time"
13
14 "github.com/bnema/gordon/internal/cli"
15 "github.com/bnema/gordon/internal/cli/handler"
16 "github.com/bnema/gordon/internal/common"
17 "github.com/bnema/gordon/pkg/docker"
18 "github.com/charmbracelet/log"
19 "github.com/spf13/cobra"
20)
21
22func NewDeployCommand(a *cli.App) *cobra.Command {
23 var port string
24 var targetDomain string
25
26 deployCmd := &cobra.Command{
27 Use: "deploy [image:tag]",
28 Short: "Deploy an image to your remote Gordon instance",
29 Args: cobra.ExactArgs(1),
30 PreRun: func(cmd *cobra.Command, args []string) {
31 if err := handler.FieldCheck(a); err != nil {
32 log.Error("Field check failed", "error", err)
33 os.Exit(1)
34 }
35 },
36 Run: func(cmd *cobra.Command, args []string) {
37 imageName := args[0]
38 log.Info("Pushing image", "image", imageName)
39
40 if err := validateInputs(imageName, port, targetDomain); err != nil {
41 log.Error("Validation failed", "error", err)
42 return
43 }
44
45 reader, actualSize, err := exportDockerImage(imageName)
46 if err != nil {
47 log.Error("Error exporting image", "error", err)
48 return
49 }
50 defer reader.Close()
51
52 log.Info("Image exported successfully", "image", imageName, "size", actualSize)
53
54 if err := deployImage(a, reader, imageName, port, targetDomain); err != nil {
55 if deployErr, ok := err.(*common.DeploymentError); ok {
56 log.Error("Deployment failed",
57 "status_code", deployErr.StatusCode,
58 "message", deployErr.Message,
59 )
60 } else {
61 log.Error("Deployment failed", "error", err)
62 }
63 return
64 }
65
66 log.Info("Deployment successful!")
67 },
68 }
69
70 deployCmd.Flags().StringVarP(&port, "port", "p", "", "Port mapping for the container and Traefik entry point")
71 deployCmd.Flags().StringVarP(&targetDomain, "target", "t", "", "Target domain for Traefik")
72
73 return deployCmd
74}
75
76func validateInputs(imageName, port, targetDomain string) error {
77 if err := handler.ValidateImageName(imageName); err != nil {
78 return err
79 }
80
81 handler.EnsureImageTag(&imageName)
82
83 if port == "" {
84 return fmt.Errorf("port is required")
85 }
86
87 return handler.ValidateTargetDomain(targetDomain)
88}
89
90func exportDockerImage(imageName string) (io.ReadCloser, int64, error) {
91 exists, err := docker.CheckIfImageExists(imageName)
92 if err != nil {
93 return nil, 0, fmt.Errorf("error checking image existence: %w", err)
94 }
95
96 var imageID string
97 if exists {
98 imageID = imageName
99 } else {
100 imageID, err = docker.GetImageIDByName(imageName)
101 if err != nil {
102 return nil, 0, fmt.Errorf("error searching for image by name: %w", err)
103 }
104 }
105
106 actualSize, err := docker.GetImageSizeFromReader(imageID)
107 if err != nil {
108 return nil, 0, fmt.Errorf("error retrieving image size: %w", err)
109 }
110
111 reader, err := docker.ExportImageFromEngine(imageID)
112 if err != nil {
113 return nil, 0, fmt.Errorf("error exporting image: %w", err)
114 }
115
116 return reader, actualSize, nil
117}
118
119func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain string) error {
120 reqPayload := common.RequestPayload{
121 Type: "deploy",
122 Payload: common.DeployPayload{
123 Port: port,
124 TargetDomain: targetDomain,
125 ImageName: imageName,
126 Data: io.NopCloser(reader),
127 },
128 }
129
130 resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/deploy")
131 if err != nil {
132 return &common.DeploymentError{
133 StatusCode: 0,
134 Message: fmt.Sprintf("error sending HTTP request: %v", err),
135 }
136 }
137
138 var deployResponse common.DeployResponse
139 if err := json.Unmarshal(resp.Body, &deployResponse); err != nil {
140 return &common.DeploymentError{
141 StatusCode: resp.StatusCode,
142 Message: fmt.Sprintf("error parsing response: %v", err),
143 RawResponse: string(resp.Body),
144 }
145 }
146
147 if !deployResponse.Success {
148 return &common.DeploymentError{
149 StatusCode: resp.StatusCode,
150 Message: deployResponse.Message,
151 RawResponse: string(resp.Body),
152 }
153 }
154
155 log.Info("Application deployed", "domain", deployResponse.Domain)
156 return waitForDeployment(deployResponse.Domain)
157}
158
159func waitForDeployment(domain string) error {
160 client := &http.Client{Timeout: 10 * time.Second}
161 maxRetries := 20
162 retryInterval := time.Second
163
164 log.Info("Waiting for deployment to be reachable")
165 for i := 0; i < maxRetries; i++ {
166 resp, err := client.Get(domain)
167 if err == nil {
168 defer resp.Body.Close()
169 if resp.StatusCode == http.StatusOK {
170 return nil
171 }
172 // Check for error messages in the response body
173 body, _ := io.ReadAll(resp.Body)
174 if strings.Contains(string(body), "failed to create container:") {
175 return fmt.Errorf("deployment failed: %s", string(body))
176 }
177 }
178 log.Warn("Deployment not ready yet, retrying", "attempt", fmt.Sprintf("%d/20", i+1))
179 time.Sleep(retryInterval)
180 }
181
182 return fmt.Errorf("deployment not ready after %d attempts, giving up", maxRetries)
183}