v0.1.12 71a4995

cli-endpoints.go RawHistoryFolder

1package handler
2
3import (
4	"encoding/json"
5	"errors"
6	"fmt"
7	"net/http"
8	"regexp"
9	"strings"
10
11	"github.com/bnema/gordon/internal/common"
12	"github.com/bnema/gordon/internal/server"
13	"github.com/bnema/gordon/internal/templating/cmdparams"
14	"github.com/bnema/gordon/pkg/docker"
15	"github.com/bnema/gordon/pkg/store"
16	"github.com/charmbracelet/log"
17	"github.com/labstack/echo/v4"
18)
19
20type InfoResponse struct {
21	Uptime  string `json:"uptime"`
22	Version string `json:"version"`
23}
24
25type DeployResponse = common.DeployResponse
26type PushResponse = common.PushResponse
27
28// sendJSONResponse is a helper function to send JSON responses
29func sendJSONResponse(c echo.Context, statusCode int, response interface{}) error {
30	// Debug
31	log.Debug("Sending JSON response", "statusCode", statusCode, "response", response)
32	return c.JSON(statusCode, response)
33}
34
35// Populate fills the InfoResponse struct with data from the App
36func (info *InfoResponse) Populate(a *server.App) {
37	info.Uptime = a.GetUptime()
38	info.Version = a.GetVersionstring()
39}
40
41// GetInfos handles GET requests on /api/ping endpoint
42func GetInfos(c echo.Context, a *server.App) error {
43	payload := new(common.RequestPayload)
44	if err := c.Bind(payload); err != nil {
45		return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload: "+err.Error())
46	}
47
48	if payload.Type != "ping" {
49		return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload type")
50	}
51
52	pingPayload, ok := payload.Payload.(common.PingPayload)
53	if !ok {
54		return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload structure")
55	}
56
57	if pingPayload.Message != "ping" {
58		return sendJSONResponse(c, http.StatusBadRequest, "Invalid ping message")
59	}
60
61	info := &InfoResponse{}
62	info.Populate(a)
63
64	return sendJSONResponse(c, http.StatusOK, info)
65}
66
67// PostPush handles the image push request
68func PostPush(c echo.Context, a *server.App) error {
69	payload, err := validateAndPreparePayload(c)
70	if err != nil {
71		return sendJSONResponse(c, http.StatusBadRequest, PushResponse{
72			Success: false,
73			Message: err.Error(),
74		})
75	}
76
77	_, err = saveAndImportImage(c, a, payload)
78	if err != nil {
79		return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
80			Success: false,
81			Message: err.Error(),
82		})
83	}
84
85	shortID := generateShortID(payload.ImageID)
86	storeIDMapping(shortID, payload.ImageID)
87
88	createContainerURL := generateCreateContainerURL(a, shortID)
89
90	return sendJSONResponse(c, http.StatusOK, common.PushResponse{
91		Success:            true,
92		Message:            "Image pushed successfully",
93		CreateContainerURL: createContainerURL,
94	})
95}
96
97// validateAndPreparePayload checks the request and prepares the payload
98func validateAndPreparePayload(c echo.Context) (*common.PushPayload, error) {
99	payload := &common.PushPayload{
100		ImageName: c.Request().Header.Get("X-Image-Name"),
101	}
102
103	if payload.ImageName == "" {
104		return nil, errors.New("Invalid image name")
105	}
106
107	return payload, nil
108}
109
110// saveAndImportImage saves the image to storage and imports it to the Docker engine
111func saveAndImportImage(c echo.Context, a *server.App, payload *common.PushPayload) (string, error) {
112	imageReader := c.Request().Body
113	defer imageReader.Close()
114
115	imageFileName := sanitizeImageFileName(payload.ImageName)
116
117	imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)
118	if err != nil {
119		return "", fmt.Errorf("Failed to save image: %v", err)
120	}
121
122	imageID, err := docker.ImportImageToEngine(imagePath)
123	if err != nil {
124		store.RemoveFromStorage(imagePath)
125		return "", fmt.Errorf("Failed to import image: %v", err)
126	}
127
128	if imageID == "" {
129		return "", errors.New("Imported image ID is empty")
130	}
131
132	err = store.RemoveFromStorage(imagePath)
133	if err != nil {
134		return "", fmt.Errorf("Failed to remove temporary image file: %v", err)
135	}
136
137	payload.ImageID = imageID
138	return imagePath, nil
139}
140
141// sanitizeImageFileName removes unwanted parts from the image name
142func sanitizeImageFileName(imageName string) string {
143	imageName = regexp.MustCompile(`^([a-zA-Z0-9\-_.]+\/)?`).ReplaceAllString(imageName, "")
144	imageName = regexp.MustCompile(`(:[a-zA-Z0-9\-_.]+)?$`).ReplaceAllString(imageName, "")
145	return imageName + ".tar"
146}
147
148// generateShortID creates a shortened version of the image ID
149func generateShortID(imageID string) string {
150	shortID := imageID[7:]
151	if len(shortID) > 12 {
152		shortID = shortID[:12]
153	}
154	return shortID
155}
156
157// storeIDMapping saves the mapping between short ID and full ID
158func storeIDMapping(shortID, fullID string) {
159	safelyInteractWithIDMap(Update, shortID, fullID)
160}
161
162// generateCreateContainerURL creates the URL for the container creation page
163func generateCreateContainerURL(a *server.App, shortID string) string {
164	return fmt.Sprintf("%s://%s%s/cc/%s",
165		a.Config.Http.Protocol(),
166		a.Config.Http.FullDomain(),
167		a.Config.Admin.Path,
168		shortID)
169}
170
171// PostDeploy handles the container deployment request
172func PostDeploy(c echo.Context, a *server.App) error {
173	payload, err := validateAndPrepareDeployPayload(c)
174	if err != nil {
175		// Debug
176		log.Error("Failed to validate and prepare payload", "error", err)
177		return sendJSONResponse(c, http.StatusBadRequest, DeployResponse{
178			Success: false,
179			Message: fmt.Sprintf("Invalid payload: %v", err),
180		})
181	}
182
183	_, err = saveAndImportDeployImage(c, a, payload)
184	if err != nil {
185		// Debug
186		log.Error("Failed to save and import image", "error", err)
187		return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
188			Success: false,
189			Message: fmt.Sprintf("Failed to save or import image: %v", err),
190		})
191	}
192
193	containerID, err := createAndStartContainer(a, payload)
194	if err != nil {
195		if strings.Contains(err.Error(), "is already in use") {
196			log.Warn("Container already exists", "error", err)
197			extractedID := extractContainerID(err.Error())
198			log.Info("Extracted container ID", "containerID", extractedID)
199
200			if extractedID == "" {
201				log.Error("Failed to extract container ID from error message")
202				return sendJSONResponse(c, http.StatusConflict, DeployResponse{
203					Success: false,
204					Message: "A container with this name already exists, but we couldn't extract its ID.",
205				})
206			}
207
208			return sendJSONResponse(c, http.StatusConflict, DeployResponse{
209				Success:     false,
210				Message:     fmt.Sprintf("A container for this deploy already exists."),
211				ContainerID: extractedID,
212			})
213		}
214		log.Error("Failed to create or start container", "error", err)
215		return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
216			Success: false,
217			Message: fmt.Sprintf("Failed to create or start container: %v", err),
218		})
219	}
220	response := DeployResponse{
221		Success:     true,
222		Message:     "Deployment successful",
223		Domain:      payload.TargetDomain,
224		ContainerID: containerID,
225	}
226
227	// Debug: Verify containerID in response
228	if response.ContainerID != containerID {
229		log.Error("Container ID mismatch", "responseID", response.ContainerID, "actualID", containerID)
230	} else {
231		log.Info("Container ID successfully stored in response", "containerID", containerID)
232	}
233
234	return sendJSONResponse(c, http.StatusOK, response)
235}
236
237// is already in use by 08433f639cb1c01254d43f234360bfa793aa8c33b29e8de8ce42b0706c0090ba. example
238func extractContainerID(errorMessage string) string {
239	re := regexp.MustCompile(`by\s+([0-9a-f]+)\.`)
240	match := re.FindStringSubmatch(errorMessage)
241	if len(match) > 1 {
242		return match[1]
243	}
244	return ""
245}
246
247// validateAndPrepareDeployPayload prepares the deployment payload
248func validateAndPrepareDeployPayload(c echo.Context) (*common.DeployPayload, error) {
249	payload := &common.DeployPayload{
250		Port:         c.Request().Header.Get("X-Ports"),
251		ImageName:    c.Request().Header.Get("X-Image-Name"),
252		TargetDomain: c.Request().Header.Get("X-Target-Domain"),
253	}
254
255	if payload.ImageName == "" {
256		return nil, errors.New("Invalid image name")
257	}
258
259	payload.Port = normalizePort(payload.Port)
260	payload.TargetDomain = normalizeTargetDomain(payload.TargetDomain)
261
262	return payload, nil
263}
264
265// normalizePort adds "/tcp" to the port if not present
266func normalizePort(port string) string {
267	if !regexp.MustCompile(`\/(tcp|udp)$`).MatchString(port) {
268		return port + "/tcp"
269	}
270	return port
271}
272
273// normalizeTargetDomain adds "https://" to the domain if not present
274func normalizeTargetDomain(domain string) string {
275	if !regexp.MustCompile(`^https?:\/\/`).MatchString(domain) {
276		return "https://" + domain
277	}
278	return domain
279}
280
281// saveAndImportDeployImage saves and imports the deployment image
282func saveAndImportDeployImage(c echo.Context, a *server.App, payload *common.DeployPayload) (string, error) {
283	imageReader := c.Request().Body
284	defer imageReader.Close()
285
286	imageFileName := sanitizeImageFileName(payload.ImageName)
287
288	imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)
289	if err != nil {
290		return "", fmt.Errorf("Failed to save image: %v", err)
291	}
292
293	imageID, err := docker.ImportImageToEngine(imagePath)
294	if err != nil {
295		store.RemoveFromStorage(imagePath)
296		return "", fmt.Errorf("Failed to import image: %v", err)
297	}
298
299	if imageID == "" {
300		return "", errors.New("Imported image ID is empty")
301	}
302
303	err = store.RemoveFromStorage(imagePath)
304	if err != nil {
305		return "", fmt.Errorf("Failed to remove temporary image file: %v", err)
306	}
307
308	payload.ImageID = imageID
309	return imagePath, nil
310}
311
312// createAndStartContainer creates and starts a new container
313func createAndStartContainer(a *server.App, payload *common.DeployPayload) (string, error) {
314	params, err := cmdparams.FromPayloadStructToCmdParams(payload, a, payload.ImageID)
315	if err != nil {
316		return "", fmt.Errorf("Failed to create command parameters: %v", err)
317	}
318
319	containerID, err := docker.CreateContainer(params)
320	if err != nil {
321		return "", fmt.Errorf("Failed to create container: %v", err)
322	}
323
324	err = docker.StartContainer(containerID)
325	if err != nil {
326		docker.RemoveContainer(containerID)
327		return "", fmt.Errorf("Failed to start container: %v", err)
328	}
329
330	return containerID, nil
331}
332
333// logResponse logs the deployment response
334func logResponse(response DeployResponse) {
335	responseJSON, _ := json.Marshal(response)
336	fmt.Printf("Server response: %s\n", string(responseJSON))
337}

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