0.1.9 85724f1

cli-endpoints.go RawHistoryFolder

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

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