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
- 153212dd7ae988d0d50ddba222c63885a8a3a2e9
- author
- Brice · · 2024-09-17 08:45 UTC
- parent
- 3cc9340
- 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 { | |||
| 81 | 81 | log.Info("Image pushed successfully", "image", imageName) | |
| 82 | 82 | ||
| 83 | 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 | 87 | 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 nil | 87 | return nil | ||
M internal/common/initconfig.go +14 -0
| @@ -248,3 +248,17 @@ func isWSL() bool { | |||
| 248 | 248 | ||
| 249 | 249 | 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 | + | } | |
| @@ -248,3 +248,17 @@ func isWSL() bool { | |||||
| 248 | 248 | ||||
| 249 | return false | 249 | 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 | |||
| 29 | 29 | return map[string]interface{}{ | |
| 30 | 30 | "Lang": yamlData, | |
| 31 | 31 | "LogoutURL": a.Config.Admin.Path + "/logout", | |
| 32 | + | "AdminPath": a.Config.Admin.Path, | |
| 32 | 33 | }, nil | |
| 33 | 34 | } | |
| 34 | 35 | ||
| @@ -39,6 +40,10 @@ func renderAdminPage(c echo.Context, a *server.App, templateName string) error { | |||
| 39 | 40 | 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 | 47 | rendererData, err := render.GetHTMLRenderer(mainPath, templateName, a.TemplateFS, a, fragmentsPath) | |
| 43 | 48 | if err != nil { | |
| 44 | 49 | 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 | }, nil | 33 | }, nil | ||
| 33 | } | 34 | } | ||
| 34 | 35 | ||||
| @@ -39,6 +40,10 @@ func renderAdminPage(c echo.Context, a *server.App, templateName string) error { | |||||
| 39 | return err | 40 | 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 err | 49 | return err | ||
M internal/httpserve/handler/cli-endpoints.go +162 -119
| @@ -2,7 +2,9 @@ package handler | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "encoding/json" | |
| 5 | + | "errors" | |
| 5 | 6 | "fmt" | |
| 7 | + | "log" | |
| 6 | 8 | "net/http" | |
| 7 | 9 | "regexp" | |
| 8 | 10 | ||
| @@ -22,123 +24,187 @@ type InfoResponse struct { | |||
| 22 | 24 | type DeployResponse = common.DeployResponse | |
| 23 | 25 | type PushResponse = common.PushResponse | |
| 24 | 26 | ||
| 27 | + | // sendJSONResponse is a helper function to send JSON responses | |
| 25 | 28 | func sendJSONResponse(c echo.Context, statusCode int, response interface{}) error { | |
| 26 | 29 | return c.JSON(statusCode, response) | |
| 27 | 30 | } | |
| 28 | 31 | ||
| 32 | + | // Populate fills the InfoResponse struct with data from the App | |
| 29 | 33 | func (info *InfoResponse) Populate(a *server.App) { | |
| 30 | 34 | info.Uptime = a.GetUptime() | |
| 31 | 35 | info.Version = a.GetVersionstring() | |
| 32 | 36 | } | |
| 33 | 37 | ||
| 34 | - | // Handle GET on /api/ping endpoint | |
| 38 | + | // GetInfos handles GET requests on /api/ping endpoint | |
| 35 | 39 | func GetInfos(c echo.Context, a *server.App) error { | |
| 36 | 40 | payload := new(common.RequestPayload) | |
| 37 | 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 | 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 | 49 | pingPayload, ok := payload.Payload.(common.PingPayload) | |
| 46 | 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 | 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 | 58 | info := &InfoResponse{} | |
| 56 | 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 | 65 | 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) { | |
| 63 | 96 | payload := &common.PushPayload{ | |
| 64 | 97 | ImageName: c.Request().Header.Get("X-Image-Name"), | |
| 65 | 98 | } | |
| 66 | 99 | ||
| 67 | 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 | 109 | imageReader := c.Request().Body | |
| 72 | 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) | |
| 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) | |
| 79 | 113 | ||
| 80 | - | // Save the image tar in the storage | |
| 81 | 114 | imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader) | |
| 82 | 115 | 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) | |
| 87 | 117 | } | |
| 88 | 118 | ||
| 89 | - | // Debug | |
| 90 | - | fmt.Printf("Image saved successfully to: %s\n", imagePath) | |
| 91 | - | ||
| 92 | - | // Import the tar in docker | |
| 93 | 119 | imageID, err := docker.ImportImageToEngine(imagePath) | |
| 94 | 120 | 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) | |
| 100 | 123 | } | |
| 101 | 124 | ||
| 102 | 125 | 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") | |
| 107 | 127 | } | |
| 108 | 128 | ||
| 109 | - | // Remove the image from the storage | |
| 110 | 129 | err = store.RemoveFromStorage(imagePath) | |
| 111 | 130 | 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) | |
| 116 | 132 | } | |
| 117 | 133 | ||
| 118 | - | fmt.Printf("Image imported successfully: %s\n", payload.ImageName) | |
| 119 | - | ||
| 120 | - | // Update the payload with the image ID | |
| 121 | 134 | payload.ImageID = imageID | |
| 135 | + | return imagePath, nil | |
| 136 | + | } | |
| 122 | 137 | ||
| 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 | + | } | |
| 125 | 144 | ||
| 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 | + | } | |
| 128 | 153 | ||
| 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 | + | } | |
| 131 | 159 | ||
| 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) | |
| 138 | 167 | } | |
| 139 | 168 | ||
| 169 | + | // PostDeploy handles the container deployment request | |
| 140 | 170 | 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) { | |
| 142 | 208 | payload := &common.DeployPayload{ | |
| 143 | 209 | Port: c.Request().Header.Get("X-Ports"), | |
| 144 | 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 | 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().Body | |
| 153 | - | defer imageReader.Close() | |
| 218 | + | payload.Port = normalizePort(payload.Port) | |
| 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) | |
| 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 | + | } | |
| 160 | 223 | ||
| 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" | |
| 164 | 228 | } | |
| 229 | + | return port | |
| 230 | + | } | |
| 165 | 231 | ||
| 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 | |
| 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 | 247 | imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader) | |
| 173 | 248 | 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) | |
| 178 | 250 | } | |
| 179 | 251 | ||
| 180 | - | // Debug | |
| 181 | - | fmt.Printf("Image saved successfully to: %s\n", imagePath) | |
| 182 | - | ||
| 183 | - | // Import the tar in docker | |
| 184 | 252 | imageID, err := docker.ImportImageToEngine(imagePath) | |
| 185 | 253 | 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) | |
| 191 | 256 | } | |
| 192 | 257 | ||
| 193 | 258 | 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") | |
| 198 | 260 | } | |
| 199 | 261 | ||
| 200 | - | // Remove the image from the storage | |
| 201 | 262 | err = store.RemoveFromStorage(imagePath) | |
| 202 | 263 | 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) | |
| 207 | 265 | } | |
| 208 | 266 | ||
| 209 | - | fmt.Printf("Image imported successfully: %s\n", payload.ImageName) | |
| 210 | - | ||
| 211 | - | // Update the payload with the image ID | |
| 212 | 267 | payload.ImageID = imageID | |
| 268 | + | return imagePath, nil | |
| 269 | + | } | |
| 213 | 270 | ||
| 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) | |
| 216 | 274 | 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) | |
| 221 | 276 | } | |
| 222 | - | // Create the container | |
| 277 | + | ||
| 223 | 278 | containerID, err := docker.CreateContainer(params) | |
| 224 | 279 | 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) | |
| 229 | 281 | } | |
| 230 | 282 | ||
| 231 | - | // Start the container | |
| 232 | 283 | err = docker.StartContainer(containerID) | |
| 233 | 284 | 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) | |
| 239 | 287 | } | |
| 240 | 288 | ||
| 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 | + | } | |
| 247 | 291 | ||
| 248 | - | // Log the response before sending | |
| 292 | + | // logResponse logs the deployment response | |
| 293 | + | func logResponse(response DeployResponse) { | |
| 249 | 294 | responseJSON, _ := json.Marshal(response) | |
| 250 | 295 | fmt.Printf("Server response: %s\n", string(responseJSON)) | |
| 251 | - | ||
| 252 | - | return c.JSON(http.StatusOK, response) | |
| 253 | 296 | } | |
| @@ -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.DeployResponse | 24 | type DeployResponse = common.DeployResponse | ||
| 23 | type PushResponse = common.PushResponse | 25 | 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 endpoint | 38 | + | // 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 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) { | |||
| 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().Body | 109 | 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 fails | 121 | + | 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 = imageID | 134 | 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 mapping | 145 | + | // 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 view | 154 | + | // 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 URL | 160 | + | // 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 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) { | |||
| 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().Body | 218 | + | 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.ImageName | 222 | + | } |
| 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 /tcp | 224 | + | // 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.TargetDomain | 234 | + | 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 fails | 254 | + | 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 = imageID | 267 | payload.ImageID = imageID | ||
| 268 | + | return imagePath, nil | |||
| 269 | + | } | |||
| 213 | 270 | ||||
| 214 | - | // Create the container using cmdparams.FromPayloadStructToCmdParams | 271 | + | // 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 container | 277 | + | |
| 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 fails | 285 | + | 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 domain | 289 | + | 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 sending | 292 | + | // 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | + | "log" | |
| 5 | 6 | "net/http" | |
| 6 | 7 | "strings" | |
| 7 | 8 | "sync" | |
| @@ -40,6 +41,7 @@ func safelyInteractWithIDMap(op MapOperation, key string, value ...string) (stri | |||
| 40 | 41 | case Update: | |
| 41 | 42 | if len(value) > 0 { | |
| 42 | 43 | idMap[key] = value[0] | |
| 44 | + | log.Printf("Updated mapping: %s -> %s", key, value[0]) | |
| 43 | 45 | } | |
| 44 | 46 | return "", true | |
| 45 | 47 | 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 "", true | 46 | return "", true | ||
| 45 | case Delete: | 47 | case Delete: | ||
M internal/httpserve/handler/createcontainer.go +68 -13
| @@ -2,6 +2,7 @@ package handler | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | + | "log" | |
| 5 | 6 | "net/http" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/gordon/internal/server" | |
| @@ -15,25 +16,30 @@ import ( | |||
| 15 | 16 | // FromShortIDToImageID converts a short image ID to a full image ID | |
| 16 | 17 | func FromShortIDToImageID(ShortID string) (string, error) { | |
| 17 | 18 | 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") | |
| 20 | 22 | } | |
| 21 | - | return "", fmt.Errorf("image ID not found") | |
| 23 | + | return imageID, nil | |
| 22 | 24 | } | |
| 23 | 25 | ||
| 24 | 26 | // CreateContainerRoute is the route for creating a new container | |
| 25 | 27 | 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") | |
| 33 | 37 | } | |
| 34 | 38 | ||
| 39 | + | log.Printf("Found mapping: %s -> %s", shortID, fullID) | |
| 40 | + | ||
| 35 | 41 | // Get the image info | |
| 36 | - | imageInfo, err := docker.GetImageInfo(imageID) | |
| 42 | + | imageInfo, err := docker.GetImageInfo(fullID) | |
| 37 | 43 | if err != nil { | |
| 38 | 44 | return sendError(c, err) | |
| 39 | 45 | } | |
| @@ -46,8 +52,8 @@ func CreateContainerGET(c echo.Context, a *server.App) error { | |||
| 46 | 52 | ||
| 47 | 53 | data := map[string]interface{}{ | |
| 48 | 54 | "Title": "Create a new container", | |
| 49 | - | "ShortID": ShortID, | |
| 50 | - | "ImageID": imageID, | |
| 55 | + | "ShortID": shortID, | |
| 56 | + | "ImageID": fullID, | |
| 51 | 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 | 125 | // CreateContainerPOST handles the create container form submission | |
| 71 | 126 | func CreateContainerPOST(c echo.Context, a *server.App) error { | |
| 72 | 127 | // 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 ID | 16 | // 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, nil | 20 | + | 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 container | 26 | // 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 URL | 28 | + | 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 ID | 31 | + | 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 info | 41 | // 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 submission | 125 | // 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 URL | 127 | // 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) { | |||
| 106 | 106 | adminGroup.GET("/manager", func(c echo.Context) error { | |
| 107 | 107 | return handler.AdminManagerRoute(c, a) | |
| 108 | 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 | 114 | // bindHTMXEndpoints binds all HTMX endpoints | |
| @@ -162,10 +165,6 @@ func bindHTMXEndpoints(e *echo.Echo, a *server.App) { | |||
| 162 | 165 | htmxGroup.GET("/create-container/:ID", func(c echo.Context) error { | |
| 163 | 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 | 168 | // Create container | |
| 170 | 169 | htmxGroup.POST("/create-container/:ID", func(c echo.Context) error { | |
| 171 | 170 | 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 endpoints | 114 | // 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 container | 168 | // 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 @@ | |||
| 24 | 24 | class="flex flex-row space-y-6 font-semibold text-gray-100 text-lg md:text-xl justify-center"> | |
| 25 | 25 | <li class=""> | |
| 26 | 26 | <span class="JetBrainsMonoBoldNF pr-1"> </span> | |
| 27 | - | <a href="manager">Manager</a> | |
| 27 | + | <a href="{{ .AdminPath }}/manager">Manager</a> | |
| 28 | 28 | </li> | |
| 29 | 29 | </ul> | |
| 30 | 30 | </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> | ||