detached 2ef2337

Refactor CLI endpoints and improve container creation

- Refactor PostPush and PostDeploy handlers for better modularity
- Add CreateContainerFullGET handler for full HTML page
- Update router to include new create-container endpoint
- Improve error handling and logging in container creation
- Update menu fragment to use dynamic AdminPath
- Add Protocol and FullDomain methods to HttpConfig
commit
2ef2337182297933b65f99e3c671e9d29fc9041a
author
Brice · · 2024-09-16 20:54 UTC
committer
Brice · · 2024-09-16 17:04 UTC
parent
0274256
signature
unsigned
10 files · +277 -177
D .github/workflows/build_dev.yml +0 -39
@@ -1,39 +0,0 @@
1-# name: Build and Test Gordon
2-
3-# on:
4-# push:
5-# branches:
6-# - "dev"
7-
8-# jobs:
9-# build-and-test:
10-# name: Build and Test
11-# runs-on: ubuntu-latest
12-# permissions:
13-# contents: read
14-# packages: write
15-# env:
16-# KO_DOCKER_REPO: ghcr.io/${{ github.repository }}
17-
18-# steps:
19-# - uses: actions/setup-go@v4
20-# with:
21-# go-version: "1.23"
22-
23-# - uses: actions/checkout@v3
24-
25-# # - name: Run tests
26-# # run: go test ./...
27-
28-# - name: Log in to GitHub Container Registry
29-# uses: docker/login-action@v2
30-# with:
31-# registry: ghcr.io
32-# username: ${{ github.actor }}
33-# password: ${{ secrets.GITHUB_TOKEN }}
34-
35-# - uses: ko-build/setup-ko@v0.6
36-
37-# - name: Build and push with ko
38-# run: |
39-# ko build --platform=linux/amd64,linux/arm64 --bare --tags=dev -B ./cmd/cli --base-import-paths=false --image-refs=gordon-dev
@@ -1,39 +0,0 @@
1-# name: Build and Test Gordon
2-
3-# on:
4-# push:
5-# branches:
6-# - "dev"
7-
8-# jobs:
9-# build-and-test:
10-# name: Build and Test
11-# runs-on: ubuntu-latest
12-# permissions:
13-# contents: read
14-# packages: write
15-# env:
16-# KO_DOCKER_REPO: ghcr.io/${{ github.repository }}
17-
18-# steps:
19-# - uses: actions/setup-go@v4
20-# with:
21-# go-version: "1.23"
22-
23-# - uses: actions/checkout@v3
24-
25-# # - name: Run tests
26-# # run: go test ./...
27-
28-# - name: Log in to GitHub Container Registry
29-# uses: docker/login-action@v2
30-# with:
31-# registry: ghcr.io
32-# username: ${{ github.actor }}
33-# password: ${{ secrets.GITHUB_TOKEN }}
34-
35-# - uses: ko-build/setup-ko@v0.6
36-
37-# - name: Build and push with ko
38-# run: |
39-# ko build --platform=linux/amd64,linux/arm64 --bare --tags=dev -B ./cmd/cli --base-import-paths=false --image-refs=gordon-dev
M internal/cli/cmd/push.go +1 -1
@@ -81,7 +81,7 @@ func pushImage(a *cli.App, imageName string) error {
8181 log.Info("Image pushed successfully", "image", imageName)
8282
8383 if pushResponse.CreateContainerURL != "" {
84- log.Info("You can create a container for this image at:", "url", pushResponse.CreateContainerURL)
84+ log.Info("Create a container for this image at:", "url", pushResponse.CreateContainerURL)
8585 }
8686
8787 return nil
@@ -81,7 +81,7 @@ func pushImage(a *cli.App, imageName string) error {
81 log.Info("Image pushed successfully", "image", imageName)81 log.Info("Image pushed successfully", "image", imageName)
82 82
83 if pushResponse.CreateContainerURL != "" {83 if pushResponse.CreateContainerURL != "" {
84- log.Info("You can create a container for this image at:", "url", pushResponse.CreateContainerURL)84+ log.Info("Create a container for this image at:", "url", pushResponse.CreateContainerURL)
85 }85 }
86 86
87 return nil87 return nil
M internal/common/initconfig.go +14 -0
@@ -248,3 +248,17 @@ func isWSL() bool {
248248
249249 return false
250250 }
251+
252+func (c *HttpConfig) Protocol() string {
253+ if c.Https {
254+ return "https"
255+ }
256+ return "http"
257+}
258+
259+func (c *HttpConfig) FullDomain() string {
260+ if c.SubDomain != "" {
261+ return fmt.Sprintf("%s.%s", c.SubDomain, c.Domain)
262+ }
263+ return c.Domain
264+}
@@ -248,3 +248,17 @@ func isWSL() bool {
248 248
249 return false249 return false
250 }250 }
251+
252+func (c *HttpConfig) Protocol() string {
253+ if c.Https {
254+ return "https"
255+ }
256+ return "http"
257+}
258+
259+func (c *HttpConfig) FullDomain() string {
260+ if c.SubDomain != "" {
261+ return fmt.Sprintf("%s.%s", c.SubDomain, c.Domain)
262+ }
263+ return c.Domain
264+}
M internal/httpserve/handler/admin-view.go +5 -0
@@ -29,6 +29,7 @@ func GetLocalizedData(c echo.Context, a *server.App) (map[string]interface{}, er
2929 return map[string]interface{}{
3030 "Lang": yamlData,
3131 "LogoutURL": a.Config.Admin.Path + "/logout",
32+ "AdminPath": a.Config.Admin.Path,
3233 }, nil
3334 }
3435
@@ -39,6 +40,10 @@ func renderAdminPage(c echo.Context, a *server.App, templateName string) error {
3940 return err
4041 }
4142
43+ data["AdminPath"] = a.Config.Admin.Path
44+ // DEBUG show the admin path here
45+ fmt.Println("AdminPath:", a.Config.Admin.Path)
46+
4247 rendererData, err := render.GetHTMLRenderer(mainPath, templateName, a.TemplateFS, a, fragmentsPath)
4348 if err != nil {
4449 return err
@@ -29,6 +29,7 @@ func GetLocalizedData(c echo.Context, a *server.App) (map[string]interface{}, er
29 return map[string]interface{}{29 return map[string]interface{}{
30 "Lang": yamlData,30 "Lang": yamlData,
31 "LogoutURL": a.Config.Admin.Path + "/logout",31 "LogoutURL": a.Config.Admin.Path + "/logout",
32+ "AdminPath": a.Config.Admin.Path,
32 }, nil33 }, nil
33 }34 }
34 35
@@ -39,6 +40,10 @@ func renderAdminPage(c echo.Context, a *server.App, templateName string) error {
39 return err40 return err
40 }41 }
41 42
43+ data["AdminPath"] = a.Config.Admin.Path
44+ // DEBUG show the admin path here
45+ fmt.Println("AdminPath:", a.Config.Admin.Path)
46+
42 rendererData, err := render.GetHTMLRenderer(mainPath, templateName, a.TemplateFS, a, fragmentsPath)47 rendererData, err := render.GetHTMLRenderer(mainPath, templateName, a.TemplateFS, a, fragmentsPath)
43 if err != nil {48 if err != nil {
44 return err49 return err
M internal/httpserve/handler/cli-endpoints.go +162 -119
@@ -2,7 +2,9 @@ package handler
22
33 import (
44 "encoding/json"
5+ "errors"
56 "fmt"
7+ "log"
68 "net/http"
79 "regexp"
810
@@ -22,123 +24,187 @@ type InfoResponse struct {
2224 type DeployResponse = common.DeployResponse
2325 type PushResponse = common.PushResponse
2426
27+// sendJSONResponse is a helper function to send JSON responses
2528 func sendJSONResponse(c echo.Context, statusCode int, response interface{}) error {
2629 return c.JSON(statusCode, response)
2730 }
2831
32+// Populate fills the InfoResponse struct with data from the App
2933 func (info *InfoResponse) Populate(a *server.App) {
3034 info.Uptime = a.GetUptime()
3135 info.Version = a.GetVersionstring()
3236 }
3337
34-// Handle GET on /api/ping endpoint
38+// GetInfos handles GET requests on /api/ping endpoint
3539 func GetInfos(c echo.Context, a *server.App) error {
3640 payload := new(common.RequestPayload)
3741 if err := c.Bind(payload); err != nil {
38- return c.JSON(http.StatusBadRequest, "Invalid payload: "+err.Error())
42+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload: "+err.Error())
3943 }
4044
4145 if payload.Type != "ping" {
42- return c.JSON(http.StatusBadRequest, "Invalid payload type")
46+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload type")
4347 }
4448
4549 pingPayload, ok := payload.Payload.(common.PingPayload)
4650 if !ok {
47- return c.JSON(http.StatusBadRequest, "Invalid payload structure")
51+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload structure")
4852 }
4953
5054 if pingPayload.Message != "ping" {
51- return c.JSON(http.StatusBadRequest, "Invalid ping message")
55+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid ping message")
5256 }
5357
54- // Prepare and populate the information
5558 info := &InfoResponse{}
5659 info.Populate(a)
5760
58- return c.JSON(http.StatusOK, info)
61+ return sendJSONResponse(c, http.StatusOK, info)
5962 }
6063
64+// PostPush handles the image push request
6165 func PostPush(c echo.Context, a *server.App) error {
62- // Initialize pushPayload object
66+ payload, err := validateAndPreparePayload(c)
67+ if err != nil {
68+ return sendJSONResponse(c, http.StatusBadRequest, PushResponse{
69+ Success: false,
70+ Message: err.Error(),
71+ })
72+ }
73+
74+ _, err = saveAndImportImage(c, a, payload)
75+ if err != nil {
76+ return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
77+ Success: false,
78+ Message: err.Error(),
79+ })
80+ }
81+
82+ shortID := generateShortID(payload.ImageID)
83+ storeIDMapping(shortID, payload.ImageID)
84+
85+ createContainerURL := generateCreateContainerURL(a, shortID)
86+
87+ return sendJSONResponse(c, http.StatusOK, common.PushResponse{
88+ Success: true,
89+ Message: "Image pushed successfully",
90+ CreateContainerURL: createContainerURL,
91+ })
92+}
93+
94+// validateAndPreparePayload checks the request and prepares the payload
95+func validateAndPreparePayload(c echo.Context) (*common.PushPayload, error) {
6396 payload := &common.PushPayload{
6497 ImageName: c.Request().Header.Get("X-Image-Name"),
6598 }
6699
67100 if payload.ImageName == "" {
68- return c.JSON(http.StatusBadRequest, "Invalid image name")
101+ return nil, errors.New("Invalid image name")
69102 }
70103
104+ return payload, nil
105+}
106+
107+// saveAndImportImage saves the image to storage and imports it to the Docker engine
108+func saveAndImportImage(c echo.Context, a *server.App, payload *common.PushPayload) (string, error) {
71109 imageReader := c.Request().Body
72110 defer imageReader.Close()
73111
74- // Rename the image to a valid name so that it can be saved (remove user/, :tag and add .tar)
75- imageFileName := payload.ImageName
76- imageFileName = regexp.MustCompile(`^([a-zA-Z0-9\-_.]+\/)?`).ReplaceAllString(imageFileName, "")
77- imageFileName = regexp.MustCompile(`(:[a-zA-Z0-9\-_.]+)?$`).ReplaceAllString(imageFileName, "")
78- imageFileName = imageFileName + ".tar"
112+ imageFileName := sanitizeImageFileName(payload.ImageName)
79113
80- // Save the image tar in the storage
81114 imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)
82115 if err != nil {
83- return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
84- Success: false,
85- Message: fmt.Sprintf("Failed to save image: %v", err),
86- })
116+ return "", fmt.Errorf("Failed to save image: %v", err)
87117 }
88118
89- // Debug
90- fmt.Printf("Image saved successfully to: %s\n", imagePath)
91-
92- // Import the tar in docker
93119 imageID, err := docker.ImportImageToEngine(imagePath)
94120 if err != nil {
95- store.RemoveFromStorage(imagePath) // Clean up the saved image if import fails
96- return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
97- Success: false,
98- Message: fmt.Sprintf("Failed to import image: %v", err),
99- })
121+ store.RemoveFromStorage(imagePath)
122+ return "", fmt.Errorf("Failed to import image: %v", err)
100123 }
101124
102125 if imageID == "" {
103- return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
104- Success: false,
105- Message: "Imported image ID is empty",
106- })
126+ return "", errors.New("Imported image ID is empty")
107127 }
108128
109- // Remove the image from the storage
110129 err = store.RemoveFromStorage(imagePath)
111130 if err != nil {
112- return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
113- Success: false,
114- Message: fmt.Sprintf("Failed to remove temporary image file: %v", err),
115- })
131+ return "", fmt.Errorf("Failed to remove temporary image file: %v", err)
116132 }
117133
118- fmt.Printf("Image imported successfully: %s\n", payload.ImageName)
119-
120- // Update the payload with the image ID
121134 payload.ImageID = imageID
135+ return imagePath, nil
136+}
122137
123- // Generate shortened ID (first 12 characters)
124- shortID := imageID[:12]
138+// sanitizeImageFileName removes unwanted parts from the image name
139+func sanitizeImageFileName(imageName string) string {
140+ imageName = regexp.MustCompile(`^([a-zA-Z0-9\-_.]+\/)?`).ReplaceAllString(imageName, "")
141+ imageName = regexp.MustCompile(`(:[a-zA-Z0-9\-_.]+)?$`).ReplaceAllString(imageName, "")
142+ return imageName + ".tar"
143+}
125144
126- // Store the mapping
127- safelyInteractWithIDMap(Update, shortID, imageID)
145+// generateShortID creates a shortened version of the image ID
146+func generateShortID(imageID string) string {
147+ shortID := imageID[7:]
148+ if len(shortID) > 12 {
149+ shortID = shortID[:12]
150+ }
151+ return shortID
152+}
128153
129- // Generate the URL for create container view
130- createContainerURL := fmt.Sprintf("%s/htmx/create-container/%s", a.Config.Server.URL, shortID)
154+// storeIDMapping saves the mapping between short ID and full ID
155+func storeIDMapping(shortID, fullID string) {
156+ log.Printf("Storing mapping: %s -> %s", shortID, fullID)
157+ safelyInteractWithIDMap(Update, shortID, fullID)
158+}
131159
132- // Return success response with the URL
133- return sendJSONResponse(c, http.StatusOK, common.PushResponse{
134- Success: true,
135- Message: "Image pushed successfully",
136- CreateContainerURL: createContainerURL,
137- })
160+// generateCreateContainerURL creates the URL for the container creation page
161+func generateCreateContainerURL(a *server.App, shortID string) string {
162+ return fmt.Sprintf("%s://%s%s/create-container/%s",
163+ a.Config.Http.Protocol(),
164+ a.Config.Http.FullDomain(),
165+ a.Config.Admin.Path,
166+ shortID)
138167 }
139168
169+// PostDeploy handles the container deployment request
140170 func PostDeploy(c echo.Context, a *server.App) error {
141- // Initialize pushPayload object
171+ payload, err := validateAndPrepareDeployPayload(c)
172+ if err != nil {
173+ return sendJSONResponse(c, http.StatusBadRequest, DeployResponse{
174+ Success: false,
175+ Message: err.Error(),
176+ })
177+ }
178+
179+ _, err = saveAndImportDeployImage(c, a, payload)
180+ if err != nil {
181+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
182+ Success: false,
183+ Message: err.Error(),
184+ })
185+ }
186+
187+ _, err = createAndStartContainer(a, payload)
188+ if err != nil {
189+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
190+ Success: false,
191+ Message: err.Error(),
192+ })
193+ }
194+
195+ response := DeployResponse{
196+ Success: true,
197+ Message: "Deployment successful",
198+ Domain: payload.TargetDomain,
199+ }
200+
201+ logResponse(response)
202+
203+ return sendJSONResponse(c, http.StatusOK, response)
204+}
205+
206+// validateAndPrepareDeployPayload prepares the deployment payload
207+func validateAndPrepareDeployPayload(c echo.Context) (*common.DeployPayload, error) {
142208 payload := &common.DeployPayload{
143209 Port: c.Request().Header.Get("X-Ports"),
144210 ImageName: c.Request().Header.Get("X-Image-Name"),
@@ -146,108 +212,85 @@ func PostDeploy(c echo.Context, a *server.App) error {
146212 }
147213
148214 if payload.ImageName == "" {
149- return c.JSON(http.StatusBadRequest, "Invalid image name")
215+ return nil, errors.New("Invalid image name")
150216 }
151217
152- imageReader := c.Request().Body
153- defer imageReader.Close()
218+ payload.Port = normalizePort(payload.Port)
219+ payload.TargetDomain = normalizeTargetDomain(payload.TargetDomain)
154220
155- // Rename the image to a valid name so that it can be saved (remove user/, :tag and add .tar)
156- imageFileName := payload.ImageName
157- imageFileName = regexp.MustCompile(`^([a-zA-Z0-9\-_.]+\/)?`).ReplaceAllString(imageFileName, "")
158- imageFileName = regexp.MustCompile(`(:[a-zA-Z0-9\-_.]+)?$`).ReplaceAllString(imageFileName, "")
159- imageFileName = imageFileName + ".tar"
221+ return payload, nil
222+}
160223
161- // Check the ports struct, if there is no /tcp, or /udp, add /tcp
162- if !regexp.MustCompile(`\/(tcp|udp)$`).MatchString(payload.Port) {
163- payload.Port = payload.Port + "/tcp"
224+// normalizePort adds "/tcp" to the port if not present
225+func normalizePort(port string) string {
226+ if !regexp.MustCompile(`\/(tcp|udp)$`).MatchString(port) {
227+ return port + "/tcp"
164228 }
229+ return port
230+}
165231
166- // Check the target domain, if there is no https:// or http://, add https://
167- if !regexp.MustCompile(`^https?:\/\/`).MatchString(payload.TargetDomain) {
168- payload.TargetDomain = "https://" + payload.TargetDomain
232+// normalizeTargetDomain adds "https://" to the domain if not present
233+func normalizeTargetDomain(domain string) string {
234+ if !regexp.MustCompile(`^https?:\/\/`).MatchString(domain) {
235+ return "https://" + domain
169236 }
237+ return domain
238+}
239+
240+// saveAndImportDeployImage saves and imports the deployment image
241+func saveAndImportDeployImage(c echo.Context, a *server.App, payload *common.DeployPayload) (string, error) {
242+ imageReader := c.Request().Body
243+ defer imageReader.Close()
244+
245+ imageFileName := sanitizeImageFileName(payload.ImageName)
170246
171- // Save the image tar in the storage
172247 imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)
173248 if err != nil {
174- return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
175- Success: false,
176- Message: fmt.Sprintf("Failed to save image: %v", err),
177- })
249+ return "", fmt.Errorf("Failed to save image: %v", err)
178250 }
179251
180- // Debug
181- fmt.Printf("Image saved successfully to: %s\n", imagePath)
182-
183- // Import the tar in docker
184252 imageID, err := docker.ImportImageToEngine(imagePath)
185253 if err != nil {
186- store.RemoveFromStorage(imagePath) // Clean up the saved image if import fails
187- return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
188- Success: false,
189- Message: fmt.Sprintf("Failed to import image: %v", err),
190- })
254+ store.RemoveFromStorage(imagePath)
255+ return "", fmt.Errorf("Failed to import image: %v", err)
191256 }
192257
193258 if imageID == "" {
194- return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
195- Success: false,
196- Message: "Imported image ID is empty",
197- })
259+ return "", errors.New("Imported image ID is empty")
198260 }
199261
200- // Remove the image from the storage
201262 err = store.RemoveFromStorage(imagePath)
202263 if err != nil {
203- return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
204- Success: false,
205- Message: fmt.Sprintf("Failed to remove temporary image file: %v", err),
206- })
264+ return "", fmt.Errorf("Failed to remove temporary image file: %v", err)
207265 }
208266
209- fmt.Printf("Image imported successfully: %s\n", payload.ImageName)
210-
211- // Update the payload with the image ID
212267 payload.ImageID = imageID
268+ return imagePath, nil
269+}
213270
214- // Create the container using cmdparams.FromPayloadStructToCmdParams
215- params, err := cmdparams.FromPayloadStructToCmdParams(payload, a, imageID)
271+// createAndStartContainer creates and starts a new container
272+func createAndStartContainer(a *server.App, payload *common.DeployPayload) (string, error) {
273+ params, err := cmdparams.FromPayloadStructToCmdParams(payload, a, payload.ImageID)
216274 if err != nil {
217- return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
218- Success: false,
219- Message: fmt.Sprintf("Failed to create command parameters: %v", err),
220- })
275+ return "", fmt.Errorf("Failed to create command parameters: %v", err)
221276 }
222- // Create the container
277+
223278 containerID, err := docker.CreateContainer(params)
224279 if err != nil {
225- return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
226- Success: false,
227- Message: fmt.Sprintf("Failed to create container: %v", err),
228- })
280+ return "", fmt.Errorf("Failed to create container: %v", err)
229281 }
230282
231- // Start the container
232283 err = docker.StartContainer(containerID)
233284 if err != nil {
234- docker.RemoveContainer(containerID) // Clean up if start fails
235- return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
236- Success: false,
237- Message: fmt.Sprintf("Failed to start container: %v", err),
238- })
285+ docker.RemoveContainer(containerID)
286+ return "", fmt.Errorf("Failed to start container: %v", err)
239287 }
240288
241- // If we arrive here, send back a success response with the target domain
242- response := DeployResponse{
243- Success: true,
244- Message: "Deployment successful",
245- Domain: payload.TargetDomain,
246- }
289+ return containerID, nil
290+}
247291
248- // Log the response before sending
292+// logResponse logs the deployment response
293+func logResponse(response DeployResponse) {
249294 responseJSON, _ := json.Marshal(response)
250295 fmt.Printf("Server response: %s\n", string(responseJSON))
251-
252- return c.JSON(http.StatusOK, response)
253296 }
@@ -2,7 +2,9 @@ package handler
2 2
3 import (3 import (
4 "encoding/json"4 "encoding/json"
5+ "errors"
5 "fmt"6 "fmt"
7+ "log"
6 "net/http"8 "net/http"
7 "regexp"9 "regexp"
8 10
@@ -22,123 +24,187 @@ type InfoResponse struct {
22 type DeployResponse = common.DeployResponse24 type DeployResponse = common.DeployResponse
23 type PushResponse = common.PushResponse25 type PushResponse = common.PushResponse
24 26
27+// sendJSONResponse is a helper function to send JSON responses
25 func sendJSONResponse(c echo.Context, statusCode int, response interface{}) error {28 func sendJSONResponse(c echo.Context, statusCode int, response interface{}) error {
26 return c.JSON(statusCode, response)29 return c.JSON(statusCode, response)
27 }30 }
28 31
32+// Populate fills the InfoResponse struct with data from the App
29 func (info *InfoResponse) Populate(a *server.App) {33 func (info *InfoResponse) Populate(a *server.App) {
30 info.Uptime = a.GetUptime()34 info.Uptime = a.GetUptime()
31 info.Version = a.GetVersionstring()35 info.Version = a.GetVersionstring()
32 }36 }
33 37
34-// Handle GET on /api/ping endpoint38+// GetInfos handles GET requests on /api/ping endpoint
35 func GetInfos(c echo.Context, a *server.App) error {39 func GetInfos(c echo.Context, a *server.App) error {
36 payload := new(common.RequestPayload)40 payload := new(common.RequestPayload)
37 if err := c.Bind(payload); err != nil {41 if err := c.Bind(payload); err != nil {
38- return c.JSON(http.StatusBadRequest, "Invalid payload: "+err.Error())42+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload: "+err.Error())
39 }43 }
40 44
41 if payload.Type != "ping" {45 if payload.Type != "ping" {
42- return c.JSON(http.StatusBadRequest, "Invalid payload type")46+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload type")
43 }47 }
44 48
45 pingPayload, ok := payload.Payload.(common.PingPayload)49 pingPayload, ok := payload.Payload.(common.PingPayload)
46 if !ok {50 if !ok {
47- return c.JSON(http.StatusBadRequest, "Invalid payload structure")51+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload structure")
48 }52 }
49 53
50 if pingPayload.Message != "ping" {54 if pingPayload.Message != "ping" {
51- return c.JSON(http.StatusBadRequest, "Invalid ping message")55+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid ping message")
52 }56 }
53 57
54- // Prepare and populate the information
55 info := &InfoResponse{}58 info := &InfoResponse{}
56 info.Populate(a)59 info.Populate(a)
57 60
58- return c.JSON(http.StatusOK, info)61+ return sendJSONResponse(c, http.StatusOK, info)
59 }62 }
60 63
64+// PostPush handles the image push request
61 func PostPush(c echo.Context, a *server.App) error {65 func PostPush(c echo.Context, a *server.App) error {
62- // Initialize pushPayload object66+ payload, err := validateAndPreparePayload(c)
67+ if err != nil {
68+ return sendJSONResponse(c, http.StatusBadRequest, PushResponse{
69+ Success: false,
70+ Message: err.Error(),
71+ })
72+ }
73+
74+ _, err = saveAndImportImage(c, a, payload)
75+ if err != nil {
76+ return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
77+ Success: false,
78+ Message: err.Error(),
79+ })
80+ }
81+
82+ shortID := generateShortID(payload.ImageID)
83+ storeIDMapping(shortID, payload.ImageID)
84+
85+ createContainerURL := generateCreateContainerURL(a, shortID)
86+
87+ return sendJSONResponse(c, http.StatusOK, common.PushResponse{
88+ Success: true,
89+ Message: "Image pushed successfully",
90+ CreateContainerURL: createContainerURL,
91+ })
92+}
93+
94+// validateAndPreparePayload checks the request and prepares the payload
95+func validateAndPreparePayload(c echo.Context) (*common.PushPayload, error) {
63 payload := &common.PushPayload{96 payload := &common.PushPayload{
64 ImageName: c.Request().Header.Get("X-Image-Name"),97 ImageName: c.Request().Header.Get("X-Image-Name"),
65 }98 }
66 99
67 if payload.ImageName == "" {100 if payload.ImageName == "" {
68- return c.JSON(http.StatusBadRequest, "Invalid image name")101+ return nil, errors.New("Invalid image name")
69 }102 }
70 103
104+ return payload, nil
105+}
106+
107+// saveAndImportImage saves the image to storage and imports it to the Docker engine
108+func saveAndImportImage(c echo.Context, a *server.App, payload *common.PushPayload) (string, error) {
71 imageReader := c.Request().Body109 imageReader := c.Request().Body
72 defer imageReader.Close()110 defer imageReader.Close()
73 111
74- // Rename the image to a valid name so that it can be saved (remove user/, :tag and add .tar)112+ imageFileName := sanitizeImageFileName(payload.ImageName)
75- imageFileName := payload.ImageName
76- imageFileName = regexp.MustCompile(`^([a-zA-Z0-9\-_.]+\/)?`).ReplaceAllString(imageFileName, "")
77- imageFileName = regexp.MustCompile(`(:[a-zA-Z0-9\-_.]+)?$`).ReplaceAllString(imageFileName, "")
78- imageFileName = imageFileName + ".tar"
79 113
80- // Save the image tar in the storage
81 imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)114 imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)
82 if err != nil {115 if err != nil {
83- return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{116+ return "", fmt.Errorf("Failed to save image: %v", err)
84- Success: false,
85- Message: fmt.Sprintf("Failed to save image: %v", err),
86- })
87 }117 }
88 118
89- // Debug
90- fmt.Printf("Image saved successfully to: %s\n", imagePath)
91-
92- // Import the tar in docker
93 imageID, err := docker.ImportImageToEngine(imagePath)119 imageID, err := docker.ImportImageToEngine(imagePath)
94 if err != nil {120 if err != nil {
95- store.RemoveFromStorage(imagePath) // Clean up the saved image if import fails121+ store.RemoveFromStorage(imagePath)
96- return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{122+ return "", fmt.Errorf("Failed to import image: %v", err)
97- Success: false,
98- Message: fmt.Sprintf("Failed to import image: %v", err),
99- })
100 }123 }
101 124
102 if imageID == "" {125 if imageID == "" {
103- return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{126+ return "", errors.New("Imported image ID is empty")
104- Success: false,
105- Message: "Imported image ID is empty",
106- })
107 }127 }
108 128
109- // Remove the image from the storage
110 err = store.RemoveFromStorage(imagePath)129 err = store.RemoveFromStorage(imagePath)
111 if err != nil {130 if err != nil {
112- return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{131+ return "", fmt.Errorf("Failed to remove temporary image file: %v", err)
113- Success: false,
114- Message: fmt.Sprintf("Failed to remove temporary image file: %v", err),
115- })
116 }132 }
117 133
118- fmt.Printf("Image imported successfully: %s\n", payload.ImageName)
119-
120- // Update the payload with the image ID
121 payload.ImageID = imageID134 payload.ImageID = imageID
135+ return imagePath, nil
136+}
122 137
123- // Generate shortened ID (first 12 characters)138+// sanitizeImageFileName removes unwanted parts from the image name
124- shortID := imageID[:12]139+func sanitizeImageFileName(imageName string) string {
140+ imageName = regexp.MustCompile(`^([a-zA-Z0-9\-_.]+\/)?`).ReplaceAllString(imageName, "")
141+ imageName = regexp.MustCompile(`(:[a-zA-Z0-9\-_.]+)?$`).ReplaceAllString(imageName, "")
142+ return imageName + ".tar"
143+}
125 144
126- // Store the mapping145+// generateShortID creates a shortened version of the image ID
127- safelyInteractWithIDMap(Update, shortID, imageID)146+func generateShortID(imageID string) string {
147+ shortID := imageID[7:]
148+ if len(shortID) > 12 {
149+ shortID = shortID[:12]
150+ }
151+ return shortID
152+}
128 153
129- // Generate the URL for create container view154+// storeIDMapping saves the mapping between short ID and full ID
130- createContainerURL := fmt.Sprintf("%s/htmx/create-container/%s", a.Config.Server.URL, shortID)155+func storeIDMapping(shortID, fullID string) {
156+ log.Printf("Storing mapping: %s -> %s", shortID, fullID)
157+ safelyInteractWithIDMap(Update, shortID, fullID)
158+}
131 159
132- // Return success response with the URL160+// generateCreateContainerURL creates the URL for the container creation page
133- return sendJSONResponse(c, http.StatusOK, common.PushResponse{161+func generateCreateContainerURL(a *server.App, shortID string) string {
134- Success: true,162+ return fmt.Sprintf("%s://%s%s/create-container/%s",
135- Message: "Image pushed successfully",163+ a.Config.Http.Protocol(),
136- CreateContainerURL: createContainerURL,164+ a.Config.Http.FullDomain(),
137- })165+ a.Config.Admin.Path,
166+ shortID)
138 }167 }
139 168
169+// PostDeploy handles the container deployment request
140 func PostDeploy(c echo.Context, a *server.App) error {170 func PostDeploy(c echo.Context, a *server.App) error {
141- // Initialize pushPayload object171+ payload, err := validateAndPrepareDeployPayload(c)
172+ if err != nil {
173+ return sendJSONResponse(c, http.StatusBadRequest, DeployResponse{
174+ Success: false,
175+ Message: err.Error(),
176+ })
177+ }
178+
179+ _, err = saveAndImportDeployImage(c, a, payload)
180+ if err != nil {
181+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
182+ Success: false,
183+ Message: err.Error(),
184+ })
185+ }
186+
187+ _, err = createAndStartContainer(a, payload)
188+ if err != nil {
189+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
190+ Success: false,
191+ Message: err.Error(),
192+ })
193+ }
194+
195+ response := DeployResponse{
196+ Success: true,
197+ Message: "Deployment successful",
198+ Domain: payload.TargetDomain,
199+ }
200+
201+ logResponse(response)
202+
203+ return sendJSONResponse(c, http.StatusOK, response)
204+}
205+
206+// validateAndPrepareDeployPayload prepares the deployment payload
207+func validateAndPrepareDeployPayload(c echo.Context) (*common.DeployPayload, error) {
142 payload := &common.DeployPayload{208 payload := &common.DeployPayload{
143 Port: c.Request().Header.Get("X-Ports"),209 Port: c.Request().Header.Get("X-Ports"),
144 ImageName: c.Request().Header.Get("X-Image-Name"),210 ImageName: c.Request().Header.Get("X-Image-Name"),
@@ -146,108 +212,85 @@ func PostDeploy(c echo.Context, a *server.App) error {
146 }212 }
147 213
148 if payload.ImageName == "" {214 if payload.ImageName == "" {
149- return c.JSON(http.StatusBadRequest, "Invalid image name")215+ return nil, errors.New("Invalid image name")
150 }216 }
151 217
152- imageReader := c.Request().Body218+ payload.Port = normalizePort(payload.Port)
153- defer imageReader.Close()219+ payload.TargetDomain = normalizeTargetDomain(payload.TargetDomain)
154 220
155- // Rename the image to a valid name so that it can be saved (remove user/, :tag and add .tar)221+ return payload, nil
156- imageFileName := payload.ImageName222+}
157- imageFileName = regexp.MustCompile(`^([a-zA-Z0-9\-_.]+\/)?`).ReplaceAllString(imageFileName, "")
158- imageFileName = regexp.MustCompile(`(:[a-zA-Z0-9\-_.]+)?$`).ReplaceAllString(imageFileName, "")
159- imageFileName = imageFileName + ".tar"
160 223
161- // Check the ports struct, if there is no /tcp, or /udp, add /tcp224+// normalizePort adds "/tcp" to the port if not present
162- if !regexp.MustCompile(`\/(tcp|udp)$`).MatchString(payload.Port) {225+func normalizePort(port string) string {
163- payload.Port = payload.Port + "/tcp"226+ if !regexp.MustCompile(`\/(tcp|udp)$`).MatchString(port) {
227+ return port + "/tcp"
164 }228 }
229+ return port
230+}
165 231
166- // Check the target domain, if there is no https:// or http://, add https://232+// normalizeTargetDomain adds "https://" to the domain if not present
167- if !regexp.MustCompile(`^https?:\/\/`).MatchString(payload.TargetDomain) {233+func normalizeTargetDomain(domain string) string {
168- payload.TargetDomain = "https://" + payload.TargetDomain234+ if !regexp.MustCompile(`^https?:\/\/`).MatchString(domain) {
235+ return "https://" + domain
169 }236 }
237+ return domain
238+}
239+
240+// saveAndImportDeployImage saves and imports the deployment image
241+func saveAndImportDeployImage(c echo.Context, a *server.App, payload *common.DeployPayload) (string, error) {
242+ imageReader := c.Request().Body
243+ defer imageReader.Close()
244+
245+ imageFileName := sanitizeImageFileName(payload.ImageName)
170 246
171- // Save the image tar in the storage
172 imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)247 imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)
173 if err != nil {248 if err != nil {
174- return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{249+ return "", fmt.Errorf("Failed to save image: %v", err)
175- Success: false,
176- Message: fmt.Sprintf("Failed to save image: %v", err),
177- })
178 }250 }
179 251
180- // Debug
181- fmt.Printf("Image saved successfully to: %s\n", imagePath)
182-
183- // Import the tar in docker
184 imageID, err := docker.ImportImageToEngine(imagePath)252 imageID, err := docker.ImportImageToEngine(imagePath)
185 if err != nil {253 if err != nil {
186- store.RemoveFromStorage(imagePath) // Clean up the saved image if import fails254+ store.RemoveFromStorage(imagePath)
187- return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{255+ return "", fmt.Errorf("Failed to import image: %v", err)
188- Success: false,
189- Message: fmt.Sprintf("Failed to import image: %v", err),
190- })
191 }256 }
192 257
193 if imageID == "" {258 if imageID == "" {
194- return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{259+ return "", errors.New("Imported image ID is empty")
195- Success: false,
196- Message: "Imported image ID is empty",
197- })
198 }260 }
199 261
200- // Remove the image from the storage
201 err = store.RemoveFromStorage(imagePath)262 err = store.RemoveFromStorage(imagePath)
202 if err != nil {263 if err != nil {
203- return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{264+ return "", fmt.Errorf("Failed to remove temporary image file: %v", err)
204- Success: false,
205- Message: fmt.Sprintf("Failed to remove temporary image file: %v", err),
206- })
207 }265 }
208 266
209- fmt.Printf("Image imported successfully: %s\n", payload.ImageName)
210-
211- // Update the payload with the image ID
212 payload.ImageID = imageID267 payload.ImageID = imageID
268+ return imagePath, nil
269+}
213 270
214- // Create the container using cmdparams.FromPayloadStructToCmdParams271+// createAndStartContainer creates and starts a new container
215- params, err := cmdparams.FromPayloadStructToCmdParams(payload, a, imageID)272+func createAndStartContainer(a *server.App, payload *common.DeployPayload) (string, error) {
273+ params, err := cmdparams.FromPayloadStructToCmdParams(payload, a, payload.ImageID)
216 if err != nil {274 if err != nil {
217- return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{275+ return "", fmt.Errorf("Failed to create command parameters: %v", err)
218- Success: false,
219- Message: fmt.Sprintf("Failed to create command parameters: %v", err),
220- })
221 }276 }
222- // Create the container277+
223 containerID, err := docker.CreateContainer(params)278 containerID, err := docker.CreateContainer(params)
224 if err != nil {279 if err != nil {
225- return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{280+ return "", fmt.Errorf("Failed to create container: %v", err)
226- Success: false,
227- Message: fmt.Sprintf("Failed to create container: %v", err),
228- })
229 }281 }
230 282
231- // Start the container
232 err = docker.StartContainer(containerID)283 err = docker.StartContainer(containerID)
233 if err != nil {284 if err != nil {
234- docker.RemoveContainer(containerID) // Clean up if start fails285+ docker.RemoveContainer(containerID)
235- return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{286+ return "", fmt.Errorf("Failed to start container: %v", err)
236- Success: false,
237- Message: fmt.Sprintf("Failed to start container: %v", err),
238- })
239 }287 }
240 288
241- // If we arrive here, send back a success response with the target domain289+ return containerID, nil
242- response := DeployResponse{290+}
243- Success: true,
244- Message: "Deployment successful",
245- Domain: payload.TargetDomain,
246- }
247 291
248- // Log the response before sending292+// logResponse logs the deployment response
293+func logResponse(response DeployResponse) {
249 responseJSON, _ := json.Marshal(response)294 responseJSON, _ := json.Marshal(response)
250 fmt.Printf("Server response: %s\n", string(responseJSON))295 fmt.Printf("Server response: %s\n", string(responseJSON))
251-
252- return c.JSON(http.StatusOK, response)
253 }296 }
M internal/httpserve/handler/containers.go +2 -0
@@ -2,6 +2,7 @@ package handler
22
33 import (
44 "fmt"
5+ "log"
56 "net/http"
67 "strings"
78 "sync"
@@ -40,6 +41,7 @@ func safelyInteractWithIDMap(op MapOperation, key string, value ...string) (stri
4041 case Update:
4142 if len(value) > 0 {
4243 idMap[key] = value[0]
44+ log.Printf("Updated mapping: %s -> %s", key, value[0])
4345 }
4446 return "", true
4547 case Delete:
@@ -2,6 +2,7 @@ package handler
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5+ "log"
5 "net/http"6 "net/http"
6 "strings"7 "strings"
7 "sync"8 "sync"
@@ -40,6 +41,7 @@ func safelyInteractWithIDMap(op MapOperation, key string, value ...string) (stri
40 case Update:41 case Update:
41 if len(value) > 0 {42 if len(value) > 0 {
42 idMap[key] = value[0]43 idMap[key] = value[0]
44+ log.Printf("Updated mapping: %s -> %s", key, value[0])
43 }45 }
44 return "", true46 return "", true
45 case Delete:47 case Delete:
M internal/httpserve/handler/createcontainer.go +68 -13
@@ -2,6 +2,7 @@ package handler
22
33 import (
44 "fmt"
5+ "log"
56 "net/http"
67
78 "github.com/bnema/gordon/internal/server"
@@ -15,25 +16,30 @@ import (
1516 // FromShortIDToImageID converts a short image ID to a full image ID
1617 func FromShortIDToImageID(ShortID string) (string, error) {
1718 imageID, exists := safelyInteractWithIDMap(Fetch, ShortID)
18- if exists {
19- return imageID, nil
19+ if !exists {
20+ log.Printf("No mapping found for short ID: %s", ShortID)
21+ return "", fmt.Errorf("image ID not found")
2022 }
21- return "", fmt.Errorf("image ID not found")
23+ return imageID, nil
2224 }
2325
2426 // CreateContainerRoute is the route for creating a new container
2527 func CreateContainerGET(c echo.Context, a *server.App) error {
26- // Retreive the ShortID of the image from the URL
27- ShortID := c.Param("ID")
28-
29- // Convert the ShortID to a full image ID
30- imageID, err := FromShortIDToImageID(ShortID)
31- if err != nil {
32- return sendError(c, err)
28+ log.Printf("Full request URL: %s", c.Request().URL.String())
29+ // Retrieve the ShortID of the image from the URL
30+ shortID := c.Param("ID")
31+ log.Printf("Received ShortID: %s", shortID)
32+
33+ fullID, exists := safelyInteractWithIDMap(Fetch, shortID)
34+ if !exists {
35+ log.Printf("No mapping found for short ID: %s", shortID)
36+ return c.String(http.StatusNotFound, "Image ID not found")
3337 }
3438
39+ log.Printf("Found mapping: %s -> %s", shortID, fullID)
40+
3541 // Get the image info
36- imageInfo, err := docker.GetImageInfo(imageID)
42+ imageInfo, err := docker.GetImageInfo(fullID)
3743 if err != nil {
3844 return sendError(c, err)
3945 }
@@ -46,8 +52,8 @@ func CreateContainerGET(c echo.Context, a *server.App) error {
4652
4753 data := map[string]interface{}{
4854 "Title": "Create a new container",
49- "ShortID": ShortID,
50- "ImageID": imageID,
55+ "ShortID": shortID,
56+ "ImageID": fullID,
5157 "ImageName": imageName,
5258 }
5359
@@ -67,6 +73,55 @@ func CreateContainerGET(c echo.Context, a *server.App) error {
6773
6874 }
6975
76+// CreateContainerFullGET handles the full HTML page for creating a new container
77+func CreateContainerFullGET(c echo.Context, a *server.App) error {
78+ // Retrieve the ShortID of the image from the URL
79+ shortID := c.Param("ID")
80+ log.Printf("Received ShortID: %s", shortID)
81+
82+ fullID, exists := safelyInteractWithIDMap(Fetch, shortID)
83+ if !exists {
84+ log.Printf("No mapping found for short ID: %s", shortID)
85+ return c.String(http.StatusNotFound, "Image ID not found")
86+ }
87+
88+ log.Printf("Found mapping: %s -> %s", shortID, fullID)
89+
90+ // Get the image info
91+ imageInfo, err := docker.GetImageInfo(fullID)
92+ if err != nil {
93+ return sendError(c, err)
94+ }
95+
96+ // Extract image name
97+ var imageName string
98+ if len(imageInfo.RepoTags) > 0 {
99+ imageName = imageInfo.RepoTags[0]
100+ }
101+
102+ data := map[string]interface{}{
103+ "Title": "Create a new container",
104+ "ShortID": shortID,
105+ "ImageID": fullID,
106+ "ImageName": imageName,
107+ "AdminPath": a.Config.Admin.Path,
108+ }
109+
110+ // Specify both admin and fragments directories
111+ rendererData, err := render.GetHTMLRenderer("html/admin", "createcontainerfull.gohtml", a.TemplateFS, a, "html/fragments")
112+ if err != nil {
113+ return sendError(c, err)
114+ }
115+
116+ renderedHTML, err := rendererData.Render(data, a)
117+ if err != nil {
118+ log.Printf("Error rendering template: %v", err)
119+ return sendError(c, err)
120+ }
121+
122+ return c.HTML(200, renderedHTML)
123+}
124+
70125 // CreateContainerPOST handles the create container form submission
71126 func CreateContainerPOST(c echo.Context, a *server.App) error {
72127 // Retreive the ShortID of the image from the URL
@@ -2,6 +2,7 @@ package handler
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5+ "log"
5 "net/http"6 "net/http"
6 7
7 "github.com/bnema/gordon/internal/server"8 "github.com/bnema/gordon/internal/server"
@@ -15,25 +16,30 @@ import (
15 // FromShortIDToImageID converts a short image ID to a full image ID16 // FromShortIDToImageID converts a short image ID to a full image ID
16 func FromShortIDToImageID(ShortID string) (string, error) {17 func FromShortIDToImageID(ShortID string) (string, error) {
17 imageID, exists := safelyInteractWithIDMap(Fetch, ShortID)18 imageID, exists := safelyInteractWithIDMap(Fetch, ShortID)
18- if exists {19+ if !exists {
19- return imageID, nil20+ log.Printf("No mapping found for short ID: %s", ShortID)
21+ return "", fmt.Errorf("image ID not found")
20 }22 }
21- return "", fmt.Errorf("image ID not found")23+ return imageID, nil
22 }24 }
23 25
24 // CreateContainerRoute is the route for creating a new container26 // CreateContainerRoute is the route for creating a new container
25 func CreateContainerGET(c echo.Context, a *server.App) error {27 func CreateContainerGET(c echo.Context, a *server.App) error {
26- // Retreive the ShortID of the image from the URL28+ log.Printf("Full request URL: %s", c.Request().URL.String())
27- ShortID := c.Param("ID")29+ // Retrieve the ShortID of the image from the URL
28-30+ shortID := c.Param("ID")
29- // Convert the ShortID to a full image ID31+ log.Printf("Received ShortID: %s", shortID)
30- imageID, err := FromShortIDToImageID(ShortID)32+
31- if err != nil {33+ fullID, exists := safelyInteractWithIDMap(Fetch, shortID)
32- return sendError(c, err)34+ if !exists {
35+ log.Printf("No mapping found for short ID: %s", shortID)
36+ return c.String(http.StatusNotFound, "Image ID not found")
33 }37 }
34 38
39+ log.Printf("Found mapping: %s -> %s", shortID, fullID)
40+
35 // Get the image info41 // Get the image info
36- imageInfo, err := docker.GetImageInfo(imageID)42+ imageInfo, err := docker.GetImageInfo(fullID)
37 if err != nil {43 if err != nil {
38 return sendError(c, err)44 return sendError(c, err)
39 }45 }
@@ -46,8 +52,8 @@ func CreateContainerGET(c echo.Context, a *server.App) error {
46 52
47 data := map[string]interface{}{53 data := map[string]interface{}{
48 "Title": "Create a new container",54 "Title": "Create a new container",
49- "ShortID": ShortID,55+ "ShortID": shortID,
50- "ImageID": imageID,56+ "ImageID": fullID,
51 "ImageName": imageName,57 "ImageName": imageName,
52 }58 }
53 59
@@ -67,6 +73,55 @@ func CreateContainerGET(c echo.Context, a *server.App) error {
67 73
68 }74 }
69 75
76+// CreateContainerFullGET handles the full HTML page for creating a new container
77+func CreateContainerFullGET(c echo.Context, a *server.App) error {
78+ // Retrieve the ShortID of the image from the URL
79+ shortID := c.Param("ID")
80+ log.Printf("Received ShortID: %s", shortID)
81+
82+ fullID, exists := safelyInteractWithIDMap(Fetch, shortID)
83+ if !exists {
84+ log.Printf("No mapping found for short ID: %s", shortID)
85+ return c.String(http.StatusNotFound, "Image ID not found")
86+ }
87+
88+ log.Printf("Found mapping: %s -> %s", shortID, fullID)
89+
90+ // Get the image info
91+ imageInfo, err := docker.GetImageInfo(fullID)
92+ if err != nil {
93+ return sendError(c, err)
94+ }
95+
96+ // Extract image name
97+ var imageName string
98+ if len(imageInfo.RepoTags) > 0 {
99+ imageName = imageInfo.RepoTags[0]
100+ }
101+
102+ data := map[string]interface{}{
103+ "Title": "Create a new container",
104+ "ShortID": shortID,
105+ "ImageID": fullID,
106+ "ImageName": imageName,
107+ "AdminPath": a.Config.Admin.Path,
108+ }
109+
110+ // Specify both admin and fragments directories
111+ rendererData, err := render.GetHTMLRenderer("html/admin", "createcontainerfull.gohtml", a.TemplateFS, a, "html/fragments")
112+ if err != nil {
113+ return sendError(c, err)
114+ }
115+
116+ renderedHTML, err := rendererData.Render(data, a)
117+ if err != nil {
118+ log.Printf("Error rendering template: %v", err)
119+ return sendError(c, err)
120+ }
121+
122+ return c.HTML(200, renderedHTML)
123+}
124+
70 // CreateContainerPOST handles the create container form submission125 // CreateContainerPOST handles the create container form submission
71 func CreateContainerPOST(c echo.Context, a *server.App) error {126 func CreateContainerPOST(c echo.Context, a *server.App) error {
72 // Retreive the ShortID of the image from the URL127 // Retreive the ShortID of the image from the URL
M internal/httpserve/router.go +3 -4
@@ -106,6 +106,9 @@ func bindAdminRoute(e *echo.Echo, a *server.App, adminPath string) {
106106 adminGroup.GET("/manager", func(c echo.Context) error {
107107 return handler.AdminManagerRoute(c, a)
108108 }, middleware.RequireLogin(a))
109+ adminGroup.GET("/create-container/:ID", func(c echo.Context) error {
110+ return handler.CreateContainerFullGET(c, a)
111+ }, middleware.RequireLogin(a))
109112 }
110113
111114 // bindHTMXEndpoints binds all HTMX endpoints
@@ -162,10 +165,6 @@ func bindHTMXEndpoints(e *echo.Echo, a *server.App) {
162165 htmxGroup.GET("/create-container/:ID", func(c echo.Context) error {
163166 return handler.CreateContainerGET(c, a)
164167 })
165- // Display create-container component
166- htmxGroup.GET("/create-container/:ShortID", func(c echo.Context) error {
167- return handler.CreateContainerGET(c, a)
168- })
169168 // Create container
170169 htmxGroup.POST("/create-container/:ID", func(c echo.Context) error {
171170 return handler.CreateContainerPOST(c, a)
@@ -106,6 +106,9 @@ func bindAdminRoute(e *echo.Echo, a *server.App, adminPath string) {
106 adminGroup.GET("/manager", func(c echo.Context) error {106 adminGroup.GET("/manager", func(c echo.Context) error {
107 return handler.AdminManagerRoute(c, a)107 return handler.AdminManagerRoute(c, a)
108 }, middleware.RequireLogin(a))108 }, middleware.RequireLogin(a))
109+ adminGroup.GET("/create-container/:ID", func(c echo.Context) error {
110+ return handler.CreateContainerFullGET(c, a)
111+ }, middleware.RequireLogin(a))
109 }112 }
110 113
111 // bindHTMXEndpoints binds all HTMX endpoints114 // bindHTMXEndpoints binds all HTMX endpoints
@@ -162,10 +165,6 @@ func bindHTMXEndpoints(e *echo.Echo, a *server.App) {
162 htmxGroup.GET("/create-container/:ID", func(c echo.Context) error {165 htmxGroup.GET("/create-container/:ID", func(c echo.Context) error {
163 return handler.CreateContainerGET(c, a)166 return handler.CreateContainerGET(c, a)
164 })167 })
165- // Display create-container component
166- htmxGroup.GET("/create-container/:ShortID", func(c echo.Context) error {
167- return handler.CreateContainerGET(c, a)
168- })
169 // Create container168 // Create container
170 htmxGroup.POST("/create-container/:ID", func(c echo.Context) error {169 htmxGroup.POST("/create-container/:ID", func(c echo.Context) error {
171 return handler.CreateContainerPOST(c, a)170 return handler.CreateContainerPOST(c, a)
A internal/templating/models/html/admin/createcontainerfull.gohtml +21 -0
@@ -0,0 +1,21 @@
1+<!DOCTYPE html>
2+<html lang="en">
3+ {{template "header.gohtml" .}}
4+
5+ <body class="bg-zinc-200 flex flex-col lg:flex-row p-0">
6+ {{template "menu.gohtml" .}}
7+ <!-- Main content here -->
8+ <div class="flex-1 flex flex-col space-y-4 pt-16 md:pt-32 md:px-6">
9+ <div class="flex flex-col md:flex-row w-full flex-grow">
10+ <div class="flex-grow bg-white p-4 md:rounded-md md:shadow mb-4">
11+ <h1 class="text-2xl font-bold mb-4">Edit Container</h1>
12+ <!-- Include the existing edit container fragment here -->
13+ {{template "createcontainer.gohtml" .}}
14+ </div>
15+ </div>
16+ </div>
17+
18+ <!-- footer -->
19+ {{template "footer.gohtml" .}}
20+ </body>
21+</html>⏎̸
@@ -0,0 +1,21 @@
1+<!DOCTYPE html>
2+<html lang="en">
3+ {{template "header.gohtml" .}}
4+
5+ <body class="bg-zinc-200 flex flex-col lg:flex-row p-0">
6+ {{template "menu.gohtml" .}}
7+ <!-- Main content here -->
8+ <div class="flex-1 flex flex-col space-y-4 pt-16 md:pt-32 md:px-6">
9+ <div class="flex flex-col md:flex-row w-full flex-grow">
10+ <div class="flex-grow bg-white p-4 md:rounded-md md:shadow mb-4">
11+ <h1 class="text-2xl font-bold mb-4">Edit Container</h1>
12+ <!-- Include the existing edit container fragment here -->
13+ {{template "createcontainer.gohtml" .}}
14+ </div>
15+ </div>
16+ </div>
17+
18+ <!-- footer -->
19+ {{template "footer.gohtml" .}}
20+ </body>
21+</html>⏎̸
M internal/templating/models/html/fragments/menu.gohtml +1 -1
@@ -24,7 +24,7 @@
2424 class="flex flex-row space-y-6 font-semibold text-gray-100 text-lg md:text-xl justify-center">
2525 <li class="">
2626 <span class="JetBrainsMonoBoldNF pr-1"> </span>
27- <a href="manager">Manager</a>
27+ <a href="{{ .AdminPath }}/manager">Manager</a>
2828 </li>
2929 </ul>
3030 </div>
@@ -24,7 +24,7 @@
24 class="flex flex-row space-y-6 font-semibold text-gray-100 text-lg md:text-xl justify-center">24 class="flex flex-row space-y-6 font-semibold text-gray-100 text-lg md:text-xl justify-center">
25 <li class="">25 <li class="">
26 <span class="JetBrainsMonoBoldNF pr-1"> </span>26 <span class="JetBrainsMonoBoldNF pr-1"> </span>
27- <a href="manager">Manager</a>27+ <a href="{{ .AdminPath }}/manager">Manager</a>
28 </li>28 </li>
29 </ul>29 </ul>
30 </div>30 </div>

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