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 "raw_response", deployErr.RawResponse)
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")
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 handler.EnsureImageTag(&imageName)
81 if err := handler.ValidatePortMapping(port); err != nil {
82 return err
83 }
84 return handler.ValidateTargetDomain(targetDomain)
85}
86
87func exportDockerImage(imageName string) (io.ReadCloser, int64, error) {
88 exists, err := docker.CheckIfImageExists(imageName)
89 if err != nil {
90 return nil, 0, fmt.Errorf("error checking image existence: %w", err)
91 }
92
93 var imageID string
94 if exists {
95 imageID = imageName
96 } else {
97 imageID, err = docker.GetImageIDByName(imageName)
98 if err != nil {
99 return nil, 0, fmt.Errorf("error searching for image by name: %w", err)
100 }
101 }
102
103 actualSize, err := docker.GetImageSizeFromReader(imageID)
104 if err != nil {
105 return nil, 0, fmt.Errorf("error retrieving image size: %w", err)
106 }
107
108 reader, err := docker.ExportImageFromEngine(imageID)
109 if err != nil {
110 return nil, 0, fmt.Errorf("error exporting image: %w", err)
111 }
112
113 return reader, actualSize, nil
114}
115
116func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain string) error {
117 reqPayload := common.RequestPayload{
118 Type: "deploy",
119 Payload: common.DeployPayload{
120 Ports: port,
121 TargetDomain: targetDomain,
122 ImageName: imageName,
123 Data: io.NopCloser(reader),
124 },
125 }
126
127 resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/deploy")
128 if err != nil {
129 return &common.DeploymentError{
130 StatusCode: 0,
131 Message: fmt.Sprintf("error sending HTTP request: %v", err),
132 }
133 }
134
135 var deployResponse common.DeployResponse
136 if err := json.Unmarshal(resp.Body, &deployResponse); err != nil {
137 return &common.DeploymentError{
138 StatusCode: resp.StatusCode,
139 Message: fmt.Sprintf("error parsing response: %v", err),
140 RawResponse: string(resp.Body),
141 }
142 }
143
144 if !deployResponse.Success {
145 return &common.DeploymentError{
146 StatusCode: resp.StatusCode,
147 Message: deployResponse.Message,
148 RawResponse: string(resp.Body),
149 }
150 }
151
152 log.Info("Application deployed", "domain", deployResponse.Domain)
153 return waitForDeployment(deployResponse.Domain)
154}
155
156func waitForDeployment(domain string) error {
157 client := &http.Client{Timeout: 10 * time.Second}
158 maxRetries := 20
159 retryInterval := time.Second
160
161 log.Info("Waiting for deployment to be reachable")
162 for i := 0; i < maxRetries; i++ {
163 resp, err := client.Get(domain)
164 if err == nil {
165 defer resp.Body.Close()
166 if resp.StatusCode == http.StatusOK {
167 return nil
168 }
169 // Check for error messages in the response body
170 body, _ := io.ReadAll(resp.Body)
171 if strings.Contains(string(body), "failed to create container:") {
172 return fmt.Errorf("deployment failed: %s", string(body))
173 }
174 }
175 log.Warn("Deployment not ready yet, retrying", "attempt", fmt.Sprintf("%d/20", i+1))
176 time.Sleep(retryInterval)
177 }
178
179 return fmt.Errorf("deployment not ready after %d attempts, giving up", maxRetries)
180}