v0.1.12 71a4995

deploy.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close