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}