Feature: Edit containers
- Introduce a transactional approach for editing containers. This ensures that all changes are applied atomically, and if any step fails, it can be rolled back. - Add functions for creating, starting, stopping, removing, and renaming containers within the transactional context. - Convert YAML configuration for containers into a struct (`YAMLContainerParams`) and add a function to convert this struct into `ContainerCommandParams`. - Add a new endpoint and handlers for editing a container. This includes a new GET endpoint to render an edit form and a POST endpoint to handle form submissions for editing containers. - Modify the `FromYAMLStructToCmdParams` function to correctly handle ports by splitting them into a slice.
- commit
- 3a3814568cdcd22084edba6b793a832cd83e9b8b
- author
- bnema · · 2023-10-06 15:13 UTC
- parent
- 4d7e697
- signature
- unsigned
9 files · +383 -74
M internal/httpserve/handler/containers.go +1 -1
| @@ -165,7 +165,7 @@ func ContainerManagerComponent(c echo.Context, a *app.App) error { | |||
| 165 | 165 | // ContainerManagerDelete handles the /container-manager/delete route | |
| 166 | 166 | func ContainerManagerDelete(c echo.Context, a *app.App) error { | |
| 167 | 167 | ID := c.Param("ID") | |
| 168 | - | err := docker.DeleteContainer(ID) | |
| 168 | + | err := docker.RemoveContainer(ID) | |
| 169 | 169 | if err != nil { | |
| 170 | 170 | return sendError(c, err) | |
| 171 | 171 | } | |
| @@ -165,7 +165,7 @@ func ContainerManagerComponent(c echo.Context, a *app.App) error { | |||||
| 165 | // ContainerManagerDelete handles the /container-manager/delete route | 165 | // ContainerManagerDelete handles the /container-manager/delete route | ||
| 166 | func ContainerManagerDelete(c echo.Context, a *app.App) error { | 166 | func ContainerManagerDelete(c echo.Context, a *app.App) error { | ||
| 167 | ID := c.Param("ID") | 167 | ID := c.Param("ID") | ||
| 168 | - | err := docker.DeleteContainer(ID) | 168 | + | err := docker.RemoveContainer(ID) |
| 169 | if err != nil { | 169 | if err != nil { | ||
| 170 | return sendError(c, err) | 170 | return sendError(c, err) | ||
| 171 | } | 171 | } | ||
M internal/httpserve/handler/createcontainer.go +2 -3
| @@ -18,7 +18,7 @@ func FromShortIDToImageID(ShortID string) (string, error) { | |||
| 18 | 18 | idMapMutex.Unlock() | |
| 19 | 19 | ||
| 20 | 20 | if !exists { | |
| 21 | - | return "", fmt.Errorf("Invalid ShortImgID") | |
| 21 | + | return "", fmt.Errorf("image ID not found") | |
| 22 | 22 | } | |
| 23 | 23 | ||
| 24 | 24 | return imageID, nil | |
| @@ -73,7 +73,6 @@ func CreateContainerGET(c echo.Context, a *app.App) error { | |||
| 73 | 73 | func CreateContainerPOST(c echo.Context, a *app.App) error { | |
| 74 | 74 | // Retreive the ShortID of the image from the URL | |
| 75 | 75 | ShortID := c.Param("ID") | |
| 76 | - | fmt.Println(ShortID) | |
| 77 | 76 | ||
| 78 | 77 | // Convert the ShortID to a full image ID | |
| 79 | 78 | imageID, err := FromShortIDToImageID(ShortID) | |
| @@ -113,7 +112,7 @@ func CreateContainerPOST(c echo.Context, a *app.App) error { | |||
| 113 | 112 | } | |
| 114 | 113 | ||
| 115 | 114 | // Create the container | |
| 116 | - | err = docker.CreateContainer(cmdParams) | |
| 115 | + | _, err = docker.CreateContainer(cmdParams) | |
| 117 | 116 | if err != nil { | |
| 118 | 117 | return sendError(c, err) | |
| 119 | 118 | } | |
| @@ -18,7 +18,7 @@ func FromShortIDToImageID(ShortID string) (string, error) { | |||||
| 18 | idMapMutex.Unlock() | 18 | idMapMutex.Unlock() | ||
| 19 | 19 | ||||
| 20 | if !exists { | 20 | if !exists { | ||
| 21 | - | return "", fmt.Errorf("Invalid ShortImgID") | 21 | + | return "", fmt.Errorf("image ID not found") |
| 22 | } | 22 | } | ||
| 23 | 23 | ||||
| 24 | return imageID, nil | 24 | return imageID, nil | ||
| @@ -73,7 +73,6 @@ func CreateContainerGET(c echo.Context, a *app.App) error { | |||||
| 73 | func CreateContainerPOST(c echo.Context, a *app.App) error { | 73 | func CreateContainerPOST(c echo.Context, a *app.App) error { | ||
| 74 | // Retreive the ShortID of the image from the URL | 74 | // Retreive the ShortID of the image from the URL | ||
| 75 | ShortID := c.Param("ID") | 75 | ShortID := c.Param("ID") | ||
| 76 | - | fmt.Println(ShortID) | |||
| 77 | 76 | ||||
| 78 | // Convert the ShortID to a full image ID | 77 | // Convert the ShortID to a full image ID | ||
| 79 | imageID, err := FromShortIDToImageID(ShortID) | 78 | imageID, err := FromShortIDToImageID(ShortID) | ||
| @@ -113,7 +112,7 @@ func CreateContainerPOST(c echo.Context, a *app.App) error { | |||||
| 113 | } | 112 | } | ||
| 114 | 113 | ||||
| 115 | // Create the container | 114 | // Create the container | ||
| 116 | - | err = docker.CreateContainer(cmdParams) | 115 | + | _, err = docker.CreateContainer(cmdParams) |
| 117 | if err != nil { | 116 | if err != nil { | ||
| 118 | return sendError(c, err) | 117 | return sendError(c, err) | ||
| 119 | } | 118 | } | ||
M internal/httpserve/handler/editcontainer.go +189 -9
| @@ -2,6 +2,7 @@ package handler | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | + | "log" | |
| 5 | 6 | "strings" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/gordon/internal/app" | |
| @@ -36,8 +37,6 @@ func generateOrderedYAML(infoMap map[string]interface{}) (string, error) { | |||
| 36 | 37 | func ContainerManagerEditGET(c echo.Context, a *app.App) error { | |
| 37 | 38 | containerID := c.Param("ID") | |
| 38 | 39 | ||
| 39 | - | fmt.Println(containerID) | |
| 40 | - | ||
| 41 | 40 | // Get the container info | |
| 42 | 41 | containerInfo, err := docker.GetContainerInfo(containerID) | |
| 43 | 42 | if err != nil { | |
| @@ -89,6 +88,7 @@ func ContainerManagerEditGET(c echo.Context, a *app.App) error { | |||
| 89 | 88 | // Pass the containerInfo to the data map | |
| 90 | 89 | data := map[string]interface{}{ | |
| 91 | 90 | "Title": "Edit container", | |
| 91 | + | "ID": containerID, | |
| 92 | 92 | "ContainerInfo": yamlString, | |
| 93 | 93 | } | |
| 94 | 94 | ||
| @@ -107,16 +107,196 @@ func ContainerManagerEditGET(c echo.Context, a *app.App) error { | |||
| 107 | 107 | return c.HTML(200, renderedHTML) | |
| 108 | 108 | } | |
| 109 | 109 | ||
| 110 | - | // ContainerManagerEditPOST is the route action for editing a container | |
| 111 | - | func ContainerManagerEditPOST(c echo.Context, a *app.App) error { | |
| 112 | - | containerID := c.Param("ID") | |
| 113 | - | fmt.Println(containerID) | |
| 110 | + | type TransactionStep func() error | |
| 111 | + | type RollbackStep func() error | |
| 114 | 112 | ||
| 115 | - | // Get the new config | |
| 116 | - | newConfig := c.FormValue("container_config") | |
| 113 | + | type TransactionQueue struct { | |
| 114 | + | NewContainerID string | |
| 115 | + | NewContainerName string | |
| 116 | + | TempContainerName string | |
| 117 | + | OldContainerID string | |
| 118 | + | OldContainerName string | |
| 119 | + | steps []TransactionStep | |
| 120 | + | rollbacks []RollbackStep | |
| 121 | + | } | |
| 117 | 122 | ||
| 118 | - | fmt.Println(newConfig) | |
| 123 | + | func (tq *TransactionQueue) Add(step TransactionStep, rollback RollbackStep) { | |
| 124 | + | tq.steps = append(tq.steps, step) | |
| 125 | + | tq.rollbacks = append(tq.rollbacks, rollback) | |
| 126 | + | } | |
| 119 | 127 | ||
| 128 | + | func (tq *TransactionQueue) Execute() error { | |
| 129 | + | var transactionErrors []error | |
| 130 | + | var rollbackErrors []error | |
| 131 | + | for i, step := range tq.steps { | |
| 132 | + | err := step() | |
| 133 | + | if err != nil { | |
| 134 | + | log.Printf("Error occurred at step index %d: %v", i, err) | |
| 135 | + | transactionErrors = append(transactionErrors, err) | |
| 136 | + | for j := i - 1; j >= 0; j-- { | |
| 137 | + | rollback := tq.rollbacks[j] | |
| 138 | + | if rollback != nil { | |
| 139 | + | if rollbackErr := rollback(); rollbackErr != nil { | |
| 140 | + | log.Printf("Failed to execute rollback at index %d: %v", j, rollbackErr) | |
| 141 | + | rollbackErrors = append(rollbackErrors, rollbackErr) | |
| 142 | + | } | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | return fmt.Errorf("transaction failed: %v; rollback errors: %v", transactionErrors, rollbackErrors) | |
| 146 | + | } | |
| 147 | + | } | |
| 120 | 148 | return nil | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | // ContainerManagerEditPOST handles the edit container form submission | |
| 152 | + | func ContainerManagerEditPOST(c echo.Context, a *app.App) error { | |
| 153 | + | tq := &TransactionQueue{} | |
| 154 | + | oldContainerID := c.Param("ID") | |
| 155 | + | ||
| 156 | + | oldContainerInfo, err := docker.GetContainerInfo(oldContainerID) | |
| 157 | + | if err != nil { | |
| 158 | + | return sendError(c, fmt.Errorf("getting old container info failed: %w", err)) | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | containerConfig := c.FormValue("container_config") | |
| 162 | + | var containerParams render.YAMLContainerParams | |
| 163 | + | err = yaml.Unmarshal([]byte(containerConfig), &containerParams) | |
| 164 | + | if err != nil { | |
| 165 | + | return sendError(c, fmt.Errorf("YAML unmarshal failed: %w", err)) | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | // Use render.FromYAMLStructToCmdParams to convert YAMLContainerParams to ContainerCommandParams | |
| 169 | + | cmdParams, err := render.FromYAMLStructToCmdParams(containerParams) | |
| 170 | + | if err != nil { | |
| 171 | + | return fmt.Errorf("failed to convert YAML: %w", err) | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | tq.OldContainerID = oldContainerID | |
| 175 | + | tq.OldContainerName = strings.TrimPrefix(oldContainerInfo.Name, "/") | |
| 176 | + | tq.NewContainerName = cmdParams.ContainerName | |
| 177 | + | tq.TempContainerName = fmt.Sprintf("%s-temp", tq.NewContainerName) | |
| 178 | + | ||
| 179 | + | // 1. Create a new container with the new configuration with a temporary name if old container name and new container name are different | |
| 180 | + | cmdParams.ContainerName = tq.TempContainerName | |
| 181 | + | ||
| 182 | + | tq.Add(CreateNewContainerStep(tq, cmdParams), RemoveNewContainerRollback(tq)) | |
| 183 | + | ||
| 184 | + | // 2. Start the new container | |
| 185 | + | tq.Add(StartNewContainerStep(tq), StopNewContainerRollback(tq)) | |
| 121 | 186 | ||
| 187 | + | // 3. Stop the old container | |
| 188 | + | tq.Add(StopOldContainerStep(tq), StartOldContainerRollback(tq)) | |
| 189 | + | ||
| 190 | + | // 4. Remove the old container | |
| 191 | + | tq.Add(RemoveOldContainerStep(tq), nil) | |
| 192 | + | ||
| 193 | + | // 5. Rename the new container to the original name | |
| 194 | + | tq.Add(RenameNewContainerStep(tq), RenameNewContainerRollback(tq)) | |
| 195 | + | ||
| 196 | + | err = tq.Execute() | |
| 197 | + | if err != nil { | |
| 198 | + | return sendError(c, fmt.Errorf("transaction failed: %w", err)) | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | return c.HTML(200, fmt.Sprintf("Successfully edited container %s", tq.NewContainerName)) | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | // CreateNewContainerStep creates a new container with the given parameters | |
| 205 | + | func CreateNewContainerStep(tq *TransactionQueue, cmdParams docker.ContainerCommandParams) TransactionStep { | |
| 206 | + | return func() error { | |
| 207 | + | var err error | |
| 208 | + | tq.NewContainerID, err = docker.CreateContainer(cmdParams) | |
| 209 | + | if err != nil { | |
| 210 | + | return fmt.Errorf("creating new container failed: %w", err) | |
| 211 | + | } | |
| 212 | + | return nil | |
| 213 | + | } | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | // StartNewContainerStep starts the new container | |
| 217 | + | func StartNewContainerStep(tq *TransactionQueue) TransactionStep { | |
| 218 | + | return func() error { | |
| 219 | + | err := docker.StartContainer(tq.NewContainerID) | |
| 220 | + | if err != nil { | |
| 221 | + | return fmt.Errorf("starting new container failed: %w", err) | |
| 222 | + | } | |
| 223 | + | return nil | |
| 224 | + | } | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | // StopOldContainerStep stops the old container | |
| 228 | + | func StopOldContainerStep(tq *TransactionQueue) TransactionStep { | |
| 229 | + | return func() error { | |
| 230 | + | err := docker.StopContainer(tq.OldContainerID) | |
| 231 | + | if err != nil { | |
| 232 | + | return fmt.Errorf("stopping old container failed: %w", err) | |
| 233 | + | } | |
| 234 | + | return nil | |
| 235 | + | } | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | // RemoveOldContainerStep removes the old container | |
| 239 | + | func RemoveOldContainerStep(tq *TransactionQueue) TransactionStep { | |
| 240 | + | return func() error { | |
| 241 | + | err := docker.RemoveContainer(tq.OldContainerID) | |
| 242 | + | if err != nil { | |
| 243 | + | return fmt.Errorf("removing old container failed: %w", err) | |
| 244 | + | } | |
| 245 | + | return nil | |
| 246 | + | } | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | // RemoveNewContainerRollback removes the new container if the transaction fails | |
| 250 | + | func RemoveNewContainerRollback(tq *TransactionQueue) RollbackStep { | |
| 251 | + | return func() error { | |
| 252 | + | err := docker.RemoveContainer(tq.NewContainerID) | |
| 253 | + | if err != nil { | |
| 254 | + | return fmt.Errorf("rollback: removing new container failed: %w", err) | |
| 255 | + | } | |
| 256 | + | return nil | |
| 257 | + | } | |
| 258 | + | } | |
| 259 | + | ||
| 260 | + | // StopNewContainerRollback stops the new container if the transaction fails | |
| 261 | + | func StopNewContainerRollback(tq *TransactionQueue) RollbackStep { | |
| 262 | + | return func() error { | |
| 263 | + | err := docker.StopContainer(tq.NewContainerID) | |
| 264 | + | if err != nil { | |
| 265 | + | return fmt.Errorf("rollback: stopping new container failed: %w", err) | |
| 266 | + | } | |
| 267 | + | return nil | |
| 268 | + | } | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | // StartOldContainerRollback starts the old container if the transaction fails | |
| 272 | + | func StartOldContainerRollback(tq *TransactionQueue) RollbackStep { | |
| 273 | + | return func() error { | |
| 274 | + | err := docker.StartContainer(tq.OldContainerID) | |
| 275 | + | if err != nil { | |
| 276 | + | return fmt.Errorf("rollback: starting old container failed: %w", err) | |
| 277 | + | } | |
| 278 | + | return nil | |
| 279 | + | } | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | // RenameNewContainerStep renames the new temporary container to the original name | |
| 283 | + | func RenameNewContainerStep(tq *TransactionQueue) TransactionStep { | |
| 284 | + | return func() error { | |
| 285 | + | err := docker.RenameContainer(tq.NewContainerID, tq.NewContainerName) | |
| 286 | + | if err != nil { | |
| 287 | + | return fmt.Errorf("renaming new container failed: %w", err) | |
| 288 | + | } | |
| 289 | + | return nil | |
| 290 | + | } | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | // RenameNewContainerRollback renames the new container back to the temporary name if it fails | |
| 294 | + | func RenameNewContainerRollback(tq *TransactionQueue) RollbackStep { | |
| 295 | + | return func() error { | |
| 296 | + | err := docker.RenameContainer(tq.NewContainerID, tq.TempContainerName) | |
| 297 | + | if err != nil { | |
| 298 | + | return fmt.Errorf("rollback: renaming new container back failed: %w", err) | |
| 299 | + | } | |
| 300 | + | return nil | |
| 301 | + | } | |
| 122 | 302 | } | |
| @@ -2,6 +2,7 @@ package handler | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | + | "log" | |||
| 5 | "strings" | 6 | "strings" | ||
| 6 | 7 | ||||
| 7 | "github.com/bnema/gordon/internal/app" | 8 | "github.com/bnema/gordon/internal/app" | ||
| @@ -36,8 +37,6 @@ func generateOrderedYAML(infoMap map[string]interface{}) (string, error) { | |||||
| 36 | func ContainerManagerEditGET(c echo.Context, a *app.App) error { | 37 | func ContainerManagerEditGET(c echo.Context, a *app.App) error { | ||
| 37 | containerID := c.Param("ID") | 38 | containerID := c.Param("ID") | ||
| 38 | 39 | ||||
| 39 | - | fmt.Println(containerID) | |||
| 40 | - | ||||
| 41 | // Get the container info | 40 | // Get the container info | ||
| 42 | containerInfo, err := docker.GetContainerInfo(containerID) | 41 | containerInfo, err := docker.GetContainerInfo(containerID) | ||
| 43 | if err != nil { | 42 | if err != nil { | ||
| @@ -89,6 +88,7 @@ func ContainerManagerEditGET(c echo.Context, a *app.App) error { | |||||
| 89 | // Pass the containerInfo to the data map | 88 | // Pass the containerInfo to the data map | ||
| 90 | data := map[string]interface{}{ | 89 | data := map[string]interface{}{ | ||
| 91 | "Title": "Edit container", | 90 | "Title": "Edit container", | ||
| 91 | + | "ID": containerID, | |||
| 92 | "ContainerInfo": yamlString, | 92 | "ContainerInfo": yamlString, | ||
| 93 | } | 93 | } | ||
| 94 | 94 | ||||
| @@ -107,16 +107,196 @@ func ContainerManagerEditGET(c echo.Context, a *app.App) error { | |||||
| 107 | return c.HTML(200, renderedHTML) | 107 | return c.HTML(200, renderedHTML) | ||
| 108 | } | 108 | } | ||
| 109 | 109 | ||||
| 110 | - | // ContainerManagerEditPOST is the route action for editing a container | 110 | + | type TransactionStep func() error |
| 111 | - | func ContainerManagerEditPOST(c echo.Context, a *app.App) error { | 111 | + | type RollbackStep func() error |
| 112 | - | containerID := c.Param("ID") | |||
| 113 | - | fmt.Println(containerID) | |||
| 114 | 112 | ||||
| 115 | - | // Get the new config | 113 | + | type TransactionQueue struct { |
| 116 | - | newConfig := c.FormValue("container_config") | 114 | + | NewContainerID string |
| 115 | + | NewContainerName string | |||
| 116 | + | TempContainerName string | |||
| 117 | + | OldContainerID string | |||
| 118 | + | OldContainerName string | |||
| 119 | + | steps []TransactionStep | |||
| 120 | + | rollbacks []RollbackStep | |||
| 121 | + | } | |||
| 117 | 122 | ||||
| 118 | - | fmt.Println(newConfig) | 123 | + | func (tq *TransactionQueue) Add(step TransactionStep, rollback RollbackStep) { |
| 124 | + | tq.steps = append(tq.steps, step) | |||
| 125 | + | tq.rollbacks = append(tq.rollbacks, rollback) | |||
| 126 | + | } | |||
| 119 | 127 | ||||
| 128 | + | func (tq *TransactionQueue) Execute() error { | |||
| 129 | + | var transactionErrors []error | |||
| 130 | + | var rollbackErrors []error | |||
| 131 | + | for i, step := range tq.steps { | |||
| 132 | + | err := step() | |||
| 133 | + | if err != nil { | |||
| 134 | + | log.Printf("Error occurred at step index %d: %v", i, err) | |||
| 135 | + | transactionErrors = append(transactionErrors, err) | |||
| 136 | + | for j := i - 1; j >= 0; j-- { | |||
| 137 | + | rollback := tq.rollbacks[j] | |||
| 138 | + | if rollback != nil { | |||
| 139 | + | if rollbackErr := rollback(); rollbackErr != nil { | |||
| 140 | + | log.Printf("Failed to execute rollback at index %d: %v", j, rollbackErr) | |||
| 141 | + | rollbackErrors = append(rollbackErrors, rollbackErr) | |||
| 142 | + | } | |||
| 143 | + | } | |||
| 144 | + | } | |||
| 145 | + | return fmt.Errorf("transaction failed: %v; rollback errors: %v", transactionErrors, rollbackErrors) | |||
| 146 | + | } | |||
| 147 | + | } | |||
| 120 | return nil | 148 | return nil | ||
| 149 | + | } | |||
| 150 | + | ||||
| 151 | + | // ContainerManagerEditPOST handles the edit container form submission | |||
| 152 | + | func ContainerManagerEditPOST(c echo.Context, a *app.App) error { | |||
| 153 | + | tq := &TransactionQueue{} | |||
| 154 | + | oldContainerID := c.Param("ID") | |||
| 155 | + | ||||
| 156 | + | oldContainerInfo, err := docker.GetContainerInfo(oldContainerID) | |||
| 157 | + | if err != nil { | |||
| 158 | + | return sendError(c, fmt.Errorf("getting old container info failed: %w", err)) | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | containerConfig := c.FormValue("container_config") | |||
| 162 | + | var containerParams render.YAMLContainerParams | |||
| 163 | + | err = yaml.Unmarshal([]byte(containerConfig), &containerParams) | |||
| 164 | + | if err != nil { | |||
| 165 | + | return sendError(c, fmt.Errorf("YAML unmarshal failed: %w", err)) | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | // Use render.FromYAMLStructToCmdParams to convert YAMLContainerParams to ContainerCommandParams | |||
| 169 | + | cmdParams, err := render.FromYAMLStructToCmdParams(containerParams) | |||
| 170 | + | if err != nil { | |||
| 171 | + | return fmt.Errorf("failed to convert YAML: %w", err) | |||
| 172 | + | } | |||
| 173 | + | ||||
| 174 | + | tq.OldContainerID = oldContainerID | |||
| 175 | + | tq.OldContainerName = strings.TrimPrefix(oldContainerInfo.Name, "/") | |||
| 176 | + | tq.NewContainerName = cmdParams.ContainerName | |||
| 177 | + | tq.TempContainerName = fmt.Sprintf("%s-temp", tq.NewContainerName) | |||
| 178 | + | ||||
| 179 | + | // 1. Create a new container with the new configuration with a temporary name if old container name and new container name are different | |||
| 180 | + | cmdParams.ContainerName = tq.TempContainerName | |||
| 181 | + | ||||
| 182 | + | tq.Add(CreateNewContainerStep(tq, cmdParams), RemoveNewContainerRollback(tq)) | |||
| 183 | + | ||||
| 184 | + | // 2. Start the new container | |||
| 185 | + | tq.Add(StartNewContainerStep(tq), StopNewContainerRollback(tq)) | |||
| 121 | 186 | ||||
| 187 | + | // 3. Stop the old container | |||
| 188 | + | tq.Add(StopOldContainerStep(tq), StartOldContainerRollback(tq)) | |||
| 189 | + | ||||
| 190 | + | // 4. Remove the old container | |||
| 191 | + | tq.Add(RemoveOldContainerStep(tq), nil) | |||
| 192 | + | ||||
| 193 | + | // 5. Rename the new container to the original name | |||
| 194 | + | tq.Add(RenameNewContainerStep(tq), RenameNewContainerRollback(tq)) | |||
| 195 | + | ||||
| 196 | + | err = tq.Execute() | |||
| 197 | + | if err != nil { | |||
| 198 | + | return sendError(c, fmt.Errorf("transaction failed: %w", err)) | |||
| 199 | + | } | |||
| 200 | + | ||||
| 201 | + | return c.HTML(200, fmt.Sprintf("Successfully edited container %s", tq.NewContainerName)) | |||
| 202 | + | } | |||
| 203 | + | ||||
| 204 | + | // CreateNewContainerStep creates a new container with the given parameters | |||
| 205 | + | func CreateNewContainerStep(tq *TransactionQueue, cmdParams docker.ContainerCommandParams) TransactionStep { | |||
| 206 | + | return func() error { | |||
| 207 | + | var err error | |||
| 208 | + | tq.NewContainerID, err = docker.CreateContainer(cmdParams) | |||
| 209 | + | if err != nil { | |||
| 210 | + | return fmt.Errorf("creating new container failed: %w", err) | |||
| 211 | + | } | |||
| 212 | + | return nil | |||
| 213 | + | } | |||
| 214 | + | } | |||
| 215 | + | ||||
| 216 | + | // StartNewContainerStep starts the new container | |||
| 217 | + | func StartNewContainerStep(tq *TransactionQueue) TransactionStep { | |||
| 218 | + | return func() error { | |||
| 219 | + | err := docker.StartContainer(tq.NewContainerID) | |||
| 220 | + | if err != nil { | |||
| 221 | + | return fmt.Errorf("starting new container failed: %w", err) | |||
| 222 | + | } | |||
| 223 | + | return nil | |||
| 224 | + | } | |||
| 225 | + | } | |||
| 226 | + | ||||
| 227 | + | // StopOldContainerStep stops the old container | |||
| 228 | + | func StopOldContainerStep(tq *TransactionQueue) TransactionStep { | |||
| 229 | + | return func() error { | |||
| 230 | + | err := docker.StopContainer(tq.OldContainerID) | |||
| 231 | + | if err != nil { | |||
| 232 | + | return fmt.Errorf("stopping old container failed: %w", err) | |||
| 233 | + | } | |||
| 234 | + | return nil | |||
| 235 | + | } | |||
| 236 | + | } | |||
| 237 | + | ||||
| 238 | + | // RemoveOldContainerStep removes the old container | |||
| 239 | + | func RemoveOldContainerStep(tq *TransactionQueue) TransactionStep { | |||
| 240 | + | return func() error { | |||
| 241 | + | err := docker.RemoveContainer(tq.OldContainerID) | |||
| 242 | + | if err != nil { | |||
| 243 | + | return fmt.Errorf("removing old container failed: %w", err) | |||
| 244 | + | } | |||
| 245 | + | return nil | |||
| 246 | + | } | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | // RemoveNewContainerRollback removes the new container if the transaction fails | |||
| 250 | + | func RemoveNewContainerRollback(tq *TransactionQueue) RollbackStep { | |||
| 251 | + | return func() error { | |||
| 252 | + | err := docker.RemoveContainer(tq.NewContainerID) | |||
| 253 | + | if err != nil { | |||
| 254 | + | return fmt.Errorf("rollback: removing new container failed: %w", err) | |||
| 255 | + | } | |||
| 256 | + | return nil | |||
| 257 | + | } | |||
| 258 | + | } | |||
| 259 | + | ||||
| 260 | + | // StopNewContainerRollback stops the new container if the transaction fails | |||
| 261 | + | func StopNewContainerRollback(tq *TransactionQueue) RollbackStep { | |||
| 262 | + | return func() error { | |||
| 263 | + | err := docker.StopContainer(tq.NewContainerID) | |||
| 264 | + | if err != nil { | |||
| 265 | + | return fmt.Errorf("rollback: stopping new container failed: %w", err) | |||
| 266 | + | } | |||
| 267 | + | return nil | |||
| 268 | + | } | |||
| 269 | + | } | |||
| 270 | + | ||||
| 271 | + | // StartOldContainerRollback starts the old container if the transaction fails | |||
| 272 | + | func StartOldContainerRollback(tq *TransactionQueue) RollbackStep { | |||
| 273 | + | return func() error { | |||
| 274 | + | err := docker.StartContainer(tq.OldContainerID) | |||
| 275 | + | if err != nil { | |||
| 276 | + | return fmt.Errorf("rollback: starting old container failed: %w", err) | |||
| 277 | + | } | |||
| 278 | + | return nil | |||
| 279 | + | } | |||
| 280 | + | } | |||
| 281 | + | ||||
| 282 | + | // RenameNewContainerStep renames the new temporary container to the original name | |||
| 283 | + | func RenameNewContainerStep(tq *TransactionQueue) TransactionStep { | |||
| 284 | + | return func() error { | |||
| 285 | + | err := docker.RenameContainer(tq.NewContainerID, tq.NewContainerName) | |||
| 286 | + | if err != nil { | |||
| 287 | + | return fmt.Errorf("renaming new container failed: %w", err) | |||
| 288 | + | } | |||
| 289 | + | return nil | |||
| 290 | + | } | |||
| 291 | + | } | |||
| 292 | + | ||||
| 293 | + | // RenameNewContainerRollback renames the new container back to the temporary name if it fails | |||
| 294 | + | func RenameNewContainerRollback(tq *TransactionQueue) RollbackStep { | |||
| 295 | + | return func() error { | |||
| 296 | + | err := docker.RenameContainer(tq.NewContainerID, tq.TempContainerName) | |||
| 297 | + | if err != nil { | |||
| 298 | + | return fmt.Errorf("rollback: renaming new container back failed: %w", err) | |||
| 299 | + | } | |||
| 300 | + | return nil | |||
| 301 | + | } | |||
| 122 | } | 302 | } | ||
M internal/templates/models/html/fragments/editcontainer.gohtml +14 -3
| @@ -1,6 +1,16 @@ | |||
| 1 | - | <div id="edit-container-{{.ID}}" class="h-fit-content"> | |
| 1 | + | <div | |
| 2 | + | id="edit-container-{{.ID}}" | |
| 3 | + | class="h-fit-content" | |
| 4 | + | hx-ext="response-targets" | |
| 5 | + | > | |
| 2 | 6 | <div id="editor"> | |
| 3 | - | <form method="post" action="/htmx/container-manager/edit/{{.ID}}"> | |
| 7 | + | <form | |
| 8 | + | method="post" | |
| 9 | + | action="/htmx/container-manager/edit/{{.ID}}" | |
| 10 | + | hx-post="/htmx/container-manager/edit/{{.ID}}" | |
| 11 | + | hx-swap="innerHTML" | |
| 12 | + | hx-target-*="#error-message-edit-container-{{.ID}}" | |
| 13 | + | > | |
| 4 | 14 | <textarea | |
| 5 | 15 | name="container_config" | |
| 6 | 16 | id="container_config" | |
| @@ -12,8 +22,9 @@ | |||
| 12 | 22 | type="submit" | |
| 13 | 23 | value="Save" | |
| 14 | 24 | id="save-button-{{.ID}}" | |
| 15 | - | class="bg-gordonmascot-mblue text-white px-4 py-2 rounded-md md:text-xl hover:bg-gordonmascot-hblue" | |
| 25 | + | class="bg-gordonmascot-lblue text-white font-bold px-8 py-1 rounded-md hover:bg-gordonmascot-mblue cursor-pointer" | |
| 16 | 26 | /> | |
| 17 | 27 | </form> | |
| 28 | + | <div id="error-message-edit-container-{{.ID}}" class="text-red-500"></div> | |
| 18 | 29 | </div> | |
| 19 | 30 | </div> | |
| @@ -1,6 +1,16 @@ | |||||
| 1 | - | <div id="edit-container-{{.ID}}" class="h-fit-content"> | 1 | + | <div |
| 2 | + | id="edit-container-{{.ID}}" | |||
| 3 | + | class="h-fit-content" | |||
| 4 | + | hx-ext="response-targets" | |||
| 5 | + | > | |||
| 2 | <div id="editor"> | 6 | <div id="editor"> | ||
| 3 | - | <form method="post" action="/htmx/container-manager/edit/{{.ID}}"> | 7 | + | <form |
| 8 | + | method="post" | |||
| 9 | + | action="/htmx/container-manager/edit/{{.ID}}" | |||
| 10 | + | hx-post="/htmx/container-manager/edit/{{.ID}}" | |||
| 11 | + | hx-swap="innerHTML" | |||
| 12 | + | hx-target-*="#error-message-edit-container-{{.ID}}" | |||
| 13 | + | > | |||
| 4 | <textarea | 14 | <textarea | ||
| 5 | name="container_config" | 15 | name="container_config" | ||
| 6 | id="container_config" | 16 | id="container_config" | ||
| @@ -12,8 +22,9 @@ | |||||
| 12 | type="submit" | 22 | type="submit" | ||
| 13 | value="Save" | 23 | value="Save" | ||
| 14 | id="save-button-{{.ID}}" | 24 | id="save-button-{{.ID}}" | ||
| 15 | - | class="bg-gordonmascot-mblue text-white px-4 py-2 rounded-md md:text-xl hover:bg-gordonmascot-hblue" | 25 | + | class="bg-gordonmascot-lblue text-white font-bold px-8 py-1 rounded-md hover:bg-gordonmascot-mblue cursor-pointer" |
| 16 | /> | 26 | /> | ||
| 17 | </form> | 27 | </form> | ||
| 28 | + | <div id="error-message-edit-container-{{.ID}}" class="text-red-500"></div> | |||
| 18 | </div> | 29 | </div> | ||
| 19 | </div> | 30 | </div> | ||
M internal/templates/render/cmdparams.go +50 -2
| @@ -19,6 +19,8 @@ func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandPar | |||
| 19 | 19 | ||
| 20 | 20 | fmt.Println("environmentSlice", environmentSlice) | |
| 21 | 21 | ||
| 22 | + | portsStr := inputs["ports"] | |
| 23 | + | ||
| 22 | 24 | params := docker.ContainerCommandParams{ | |
| 23 | 25 | IsSSL: inputs["container_protocol"] == "https", | |
| 24 | 26 | ContainerName: inputs["container_name"], | |
| @@ -26,7 +28,7 @@ func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandPar | |||
| 26 | 28 | Domain: inputs["container_domain"], | |
| 27 | 29 | ImageName: inputs["image_name"], | |
| 28 | 30 | ImageID: inputs["image_id"], | |
| 29 | - | Ports: inputs["ports"], | |
| 31 | + | Ports: strings.Split(portsStr, ","), | |
| 30 | 32 | Restart: inputs["restart"], | |
| 31 | 33 | Volumes: volumeSlice, | |
| 32 | 34 | Environment: environmentSlice, | |
| @@ -34,7 +36,7 @@ func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandPar | |||
| 34 | 36 | } | |
| 35 | 37 | ||
| 36 | 38 | // Retain the port number for the container host for the load balancer | |
| 37 | - | exposedPort := strings.Split(params.Ports, ":")[1] | |
| 39 | + | exposedPort := strings.Split(params.Ports[0], ":")[0] | |
| 38 | 40 | ||
| 39 | 41 | entryPoint := "web" | |
| 40 | 42 | if params.IsSSL { | |
| @@ -57,3 +59,49 @@ func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandPar | |||
| 57 | 59 | ||
| 58 | 60 | return params, nil | |
| 59 | 61 | } | |
| 62 | + | ||
| 63 | + | type YAMLContainerParams struct { | |
| 64 | + | Name string `yaml:"Name"` | |
| 65 | + | Image string `yaml:"Image"` | |
| 66 | + | Hostname string `yaml:"Hostname"` | |
| 67 | + | Ports []string `yaml:"Ports"` | |
| 68 | + | Volumes []string `yaml:"Volumes"` | |
| 69 | + | Environment []string `yaml:"Environment"` | |
| 70 | + | Labels map[string]string `yaml:"Labels"` | |
| 71 | + | // Network is a slice of strings in the textarea | |
| 72 | + | Network []string `yaml:"Network"` | |
| 73 | + | Restart string `yaml:"Restart"` | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | // FromYAMLStructToCmdParams converts a YAMLContainerParams struct to a ContainerCommandParams struct | |
| 77 | + | func FromYAMLStructToCmdParams(yamlParams YAMLContainerParams) (docker.ContainerCommandParams, error) { | |
| 78 | + | ||
| 79 | + | // with the image name find the image id | |
| 80 | + | imageID, err := docker.GetImageID(yamlParams.Image) | |
| 81 | + | if err != nil { | |
| 82 | + | return docker.ContainerCommandParams{}, err | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | // Convert the map of labels to a slice of strings | |
| 86 | + | var labels []string | |
| 87 | + | for k, v := range yamlParams.Labels { | |
| 88 | + | labels = append(labels, fmt.Sprintf("%s=%s", k, v)) | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | // Convert the slice of strings to a comma separated string | |
| 92 | + | network := strings.Join(yamlParams.Network, ",") | |
| 93 | + | ||
| 94 | + | params := docker.ContainerCommandParams{ | |
| 95 | + | ContainerName: yamlParams.Name, | |
| 96 | + | ImageName: yamlParams.Image, | |
| 97 | + | ImageID: imageID, | |
| 98 | + | Ports: yamlParams.Ports, | |
| 99 | + | Volumes: yamlParams.Volumes, | |
| 100 | + | Environment: yamlParams.Environment, | |
| 101 | + | Labels: labels, | |
| 102 | + | Network: network, | |
| 103 | + | Restart: yamlParams.Restart, | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | return params, nil | |
| 107 | + | } | |
| @@ -19,6 +19,8 @@ func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandPar | |||||
| 19 | 19 | ||||
| 20 | fmt.Println("environmentSlice", environmentSlice) | 20 | fmt.Println("environmentSlice", environmentSlice) | ||
| 21 | 21 | ||||
| 22 | + | portsStr := inputs["ports"] | |||
| 23 | + | ||||
| 22 | params := docker.ContainerCommandParams{ | 24 | params := docker.ContainerCommandParams{ | ||
| 23 | IsSSL: inputs["container_protocol"] == "https", | 25 | IsSSL: inputs["container_protocol"] == "https", | ||
| 24 | ContainerName: inputs["container_name"], | 26 | ContainerName: inputs["container_name"], | ||
| @@ -26,7 +28,7 @@ func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandPar | |||||
| 26 | Domain: inputs["container_domain"], | 28 | Domain: inputs["container_domain"], | ||
| 27 | ImageName: inputs["image_name"], | 29 | ImageName: inputs["image_name"], | ||
| 28 | ImageID: inputs["image_id"], | 30 | ImageID: inputs["image_id"], | ||
| 29 | - | Ports: inputs["ports"], | 31 | + | Ports: strings.Split(portsStr, ","), |
| 30 | Restart: inputs["restart"], | 32 | Restart: inputs["restart"], | ||
| 31 | Volumes: volumeSlice, | 33 | Volumes: volumeSlice, | ||
| 32 | Environment: environmentSlice, | 34 | Environment: environmentSlice, | ||
| @@ -34,7 +36,7 @@ func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandPar | |||||
| 34 | } | 36 | } | ||
| 35 | 37 | ||||
| 36 | // Retain the port number for the container host for the load balancer | 38 | // Retain the port number for the container host for the load balancer | ||
| 37 | - | exposedPort := strings.Split(params.Ports, ":")[1] | 39 | + | exposedPort := strings.Split(params.Ports[0], ":")[0] |
| 38 | 40 | ||||
| 39 | entryPoint := "web" | 41 | entryPoint := "web" | ||
| 40 | if params.IsSSL { | 42 | if params.IsSSL { | ||
| @@ -57,3 +59,49 @@ func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandPar | |||||
| 57 | 59 | ||||
| 58 | return params, nil | 60 | return params, nil | ||
| 59 | } | 61 | } | ||
| 62 | + | ||||
| 63 | + | type YAMLContainerParams struct { | |||
| 64 | + | Name string `yaml:"Name"` | |||
| 65 | + | Image string `yaml:"Image"` | |||
| 66 | + | Hostname string `yaml:"Hostname"` | |||
| 67 | + | Ports []string `yaml:"Ports"` | |||
| 68 | + | Volumes []string `yaml:"Volumes"` | |||
| 69 | + | Environment []string `yaml:"Environment"` | |||
| 70 | + | Labels map[string]string `yaml:"Labels"` | |||
| 71 | + | // Network is a slice of strings in the textarea | |||
| 72 | + | Network []string `yaml:"Network"` | |||
| 73 | + | Restart string `yaml:"Restart"` | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | // FromYAMLStructToCmdParams converts a YAMLContainerParams struct to a ContainerCommandParams struct | |||
| 77 | + | func FromYAMLStructToCmdParams(yamlParams YAMLContainerParams) (docker.ContainerCommandParams, error) { | |||
| 78 | + | ||||
| 79 | + | // with the image name find the image id | |||
| 80 | + | imageID, err := docker.GetImageID(yamlParams.Image) | |||
| 81 | + | if err != nil { | |||
| 82 | + | return docker.ContainerCommandParams{}, err | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | // Convert the map of labels to a slice of strings | |||
| 86 | + | var labels []string | |||
| 87 | + | for k, v := range yamlParams.Labels { | |||
| 88 | + | labels = append(labels, fmt.Sprintf("%s=%s", k, v)) | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | // Convert the slice of strings to a comma separated string | |||
| 92 | + | network := strings.Join(yamlParams.Network, ",") | |||
| 93 | + | ||||
| 94 | + | params := docker.ContainerCommandParams{ | |||
| 95 | + | ContainerName: yamlParams.Name, | |||
| 96 | + | ImageName: yamlParams.Image, | |||
| 97 | + | ImageID: imageID, | |||
| 98 | + | Ports: yamlParams.Ports, | |||
| 99 | + | Volumes: yamlParams.Volumes, | |||
| 100 | + | Environment: yamlParams.Environment, | |||
| 101 | + | Labels: labels, | |||
| 102 | + | Network: network, | |||
| 103 | + | Restart: yamlParams.Restart, | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | return params, nil | |||
| 107 | + | } | |||
M internal/webui/public/assets/css/tailwind.css +9 -17
| @@ -689,8 +689,8 @@ video { | |||
| 689 | 689 | table-layout: auto; | |
| 690 | 690 | } | |
| 691 | 691 | ||
| 692 | - | .table-fixed { | |
| 693 | - | table-layout: fixed; | |
| 692 | + | .cursor-pointer { | |
| 693 | + | cursor: pointer; | |
| 694 | 694 | } | |
| 695 | 695 | ||
| 696 | 696 | .flex-row { | |
| @@ -791,11 +791,6 @@ video { | |||
| 791 | 791 | background-color: rgb(0 123 192 / var(--tw-bg-opacity)); | |
| 792 | 792 | } | |
| 793 | 793 | ||
| 794 | - | .bg-gordonmascot-mblue { | |
| 795 | - | --tw-bg-opacity: 1; | |
| 796 | - | background-color: rgb(0 82 147 / var(--tw-bg-opacity)); | |
| 797 | - | } | |
| 798 | - | ||
| 799 | 794 | .bg-gray-100 { | |
| 800 | 795 | --tw-bg-opacity: 1; | |
| 801 | 796 | background-color: rgb(243 244 246 / var(--tw-bg-opacity)); | |
| @@ -852,6 +847,11 @@ video { | |||
| 852 | 847 | padding-right: 1rem; | |
| 853 | 848 | } | |
| 854 | 849 | ||
| 850 | + | .px-8 { | |
| 851 | + | padding-left: 2rem; | |
| 852 | + | padding-right: 2rem; | |
| 853 | + | } | |
| 854 | + | ||
| 855 | 855 | .py-1 { | |
| 856 | 856 | padding-top: 0.25rem; | |
| 857 | 857 | padding-bottom: 0.25rem; | |
| @@ -866,10 +866,6 @@ video { | |||
| 866 | 866 | padding-bottom: 1rem; | |
| 867 | 867 | } | |
| 868 | 868 | ||
| 869 | - | .text-left { | |
| 870 | - | text-align: left; | |
| 871 | - | } | |
| 872 | - | ||
| 873 | 869 | .text-center { | |
| 874 | 870 | text-align: center; | |
| 875 | 871 | } | |
| @@ -917,10 +913,6 @@ video { | |||
| 917 | 913 | font-weight: 700; | |
| 918 | 914 | } | |
| 919 | 915 | ||
| 920 | - | .font-light { | |
| 921 | - | font-weight: 300; | |
| 922 | - | } | |
| 923 | - | ||
| 924 | 916 | .font-semibold { | |
| 925 | 917 | font-weight: 600; | |
| 926 | 918 | } | |
| @@ -1130,9 +1122,9 @@ video { | |||
| 1130 | 1122 | background-color: rgb(29 78 216 / var(--tw-bg-opacity)); | |
| 1131 | 1123 | } | |
| 1132 | 1124 | ||
| 1133 | - | .hover\:bg-gordonmascot-hblue:hover { | |
| 1125 | + | .hover\:bg-gordonmascot-mblue:hover { | |
| 1134 | 1126 | --tw-bg-opacity: 1; | |
| 1135 | - | background-color: rgb(1 30 92 / var(--tw-bg-opacity)); | |
| 1127 | + | background-color: rgb(0 82 147 / var(--tw-bg-opacity)); | |
| 1136 | 1128 | } | |
| 1137 | 1129 | ||
| 1138 | 1130 | @media (min-width: 640px) { | |
| @@ -689,8 +689,8 @@ video { | |||||
| 689 | table-layout: auto; | 689 | table-layout: auto; | ||
| 690 | } | 690 | } | ||
| 691 | 691 | ||||
| 692 | - | .table-fixed { | 692 | + | .cursor-pointer { |
| 693 | - | table-layout: fixed; | 693 | + | cursor: pointer; |
| 694 | } | 694 | } | ||
| 695 | 695 | ||||
| 696 | .flex-row { | 696 | .flex-row { | ||
| @@ -791,11 +791,6 @@ video { | |||||
| 791 | background-color: rgb(0 123 192 / var(--tw-bg-opacity)); | 791 | background-color: rgb(0 123 192 / var(--tw-bg-opacity)); | ||
| 792 | } | 792 | } | ||
| 793 | 793 | ||||
| 794 | - | .bg-gordonmascot-mblue { | |||
| 795 | - | --tw-bg-opacity: 1; | |||
| 796 | - | background-color: rgb(0 82 147 / var(--tw-bg-opacity)); | |||
| 797 | - | } | |||
| 798 | - | ||||
| 799 | .bg-gray-100 { | 794 | .bg-gray-100 { | ||
| 800 | --tw-bg-opacity: 1; | 795 | --tw-bg-opacity: 1; | ||
| 801 | background-color: rgb(243 244 246 / var(--tw-bg-opacity)); | 796 | background-color: rgb(243 244 246 / var(--tw-bg-opacity)); | ||
| @@ -852,6 +847,11 @@ video { | |||||
| 852 | padding-right: 1rem; | 847 | padding-right: 1rem; | ||
| 853 | } | 848 | } | ||
| 854 | 849 | ||||
| 850 | + | .px-8 { | |||
| 851 | + | padding-left: 2rem; | |||
| 852 | + | padding-right: 2rem; | |||
| 853 | + | } | |||
| 854 | + | ||||
| 855 | .py-1 { | 855 | .py-1 { | ||
| 856 | padding-top: 0.25rem; | 856 | padding-top: 0.25rem; | ||
| 857 | padding-bottom: 0.25rem; | 857 | padding-bottom: 0.25rem; | ||
| @@ -866,10 +866,6 @@ video { | |||||
| 866 | padding-bottom: 1rem; | 866 | padding-bottom: 1rem; | ||
| 867 | } | 867 | } | ||
| 868 | 868 | ||||
| 869 | - | .text-left { | |||
| 870 | - | text-align: left; | |||
| 871 | - | } | |||
| 872 | - | ||||
| 873 | .text-center { | 869 | .text-center { | ||
| 874 | text-align: center; | 870 | text-align: center; | ||
| 875 | } | 871 | } | ||
| @@ -917,10 +913,6 @@ video { | |||||
| 917 | font-weight: 700; | 913 | font-weight: 700; | ||
| 918 | } | 914 | } | ||
| 919 | 915 | ||||
| 920 | - | .font-light { | |||
| 921 | - | font-weight: 300; | |||
| 922 | - | } | |||
| 923 | - | ||||
| 924 | .font-semibold { | 916 | .font-semibold { | ||
| 925 | font-weight: 600; | 917 | font-weight: 600; | ||
| 926 | } | 918 | } | ||
| @@ -1130,9 +1122,9 @@ video { | |||||
| 1130 | background-color: rgb(29 78 216 / var(--tw-bg-opacity)); | 1122 | background-color: rgb(29 78 216 / var(--tw-bg-opacity)); | ||
| 1131 | } | 1123 | } | ||
| 1132 | 1124 | ||||
| 1133 | - | .hover\:bg-gordonmascot-hblue:hover { | 1125 | + | .hover\:bg-gordonmascot-mblue:hover { |
| 1134 | --tw-bg-opacity: 1; | 1126 | --tw-bg-opacity: 1; | ||
| 1135 | - | background-color: rgb(1 30 92 / var(--tw-bg-opacity)); | 1127 | + | background-color: rgb(0 82 147 / var(--tw-bg-opacity)); |
| 1136 | } | 1128 | } | ||
| 1137 | 1129 | ||||
| 1138 | @media (min-width: 640px) { | 1130 | @media (min-width: 640px) { | ||
M internal/webui/public/assets/js/custom.js +21 -11
| @@ -44,14 +44,24 @@ document.body.addEventListener("htmx:afterSwap", function (event) { | |||
| 44 | 44 | }); | |
| 45 | 45 | }); | |
| 46 | 46 | ||
| 47 | - | // Attach event listeners to buttons that needs to set a full location reload | |
| 48 | - | document.body.addEventListener("htmx:afterOnLoad", function (event) { | |
| 49 | - | const buttons = document.querySelectorAll('[id^="start-button-"], [id^="stop-button-"], [id^="remove-button-img-"], [id^="create-container-submit"], [id^="save-button-"]'); | |
| 50 | - | buttons.forEach(button => { | |
| 51 | - | button.addEventListener('click', function(event) { | |
| 52 | - | setTimeout(function() { | |
| 53 | - | location.reload(); | |
| 54 | - | }, 5000); | |
| 55 | - | }); | |
| 56 | - | }); | |
| 57 | - | });⏎̸ | |
| 47 | + | // // Attach event listeners to buttons that needs to set a full location reload | |
| 48 | + | // document.body.addEventListener("htmx:afterSwap", function (event) { | |
| 49 | + | // const xhr = event.detail.xhr; | |
| 50 | + | // const statusCode = xhr.status; | |
| 51 | + | ||
| 52 | + | // // Check for 500 status code | |
| 53 | + | // if (statusCode >= 500 && statusCode < 600) { | |
| 54 | + | // // Handle the error (e.g., log it or show an alert) | |
| 55 | + | // console.error("Server returned error: ", statusCode); | |
| 56 | + | // return; // Do not reload the page | |
| 57 | + | // } | |
| 58 | + | ||
| 59 | + | // const buttons = document.querySelectorAll('[id^="start-button-"], [id^="stop-button-"], [id^="remove-button-img-"], [id^="create-container-submit"], [id^="save-button-"]'); | |
| 60 | + | // buttons.forEach(button => { | |
| 61 | + | // button.addEventListener('click', function(event) { | |
| 62 | + | // setTimeout(function() { | |
| 63 | + | // location.reload(); | |
| 64 | + | // }, 5000); | |
| 65 | + | // }); | |
| 66 | + | // }); | |
| 67 | + | // }); | |
| @@ -44,14 +44,24 @@ document.body.addEventListener("htmx:afterSwap", function (event) { | |||||
| 44 | }); | 44 | }); | ||
| 45 | }); | 45 | }); | ||
| 46 | 46 | ||||
| 47 | - | // Attach event listeners to buttons that needs to set a full location reload | 47 | + | // // Attach event listeners to buttons that needs to set a full location reload |
| 48 | - | document.body.addEventListener("htmx:afterOnLoad", function (event) { | 48 | + | // document.body.addEventListener("htmx:afterSwap", function (event) { |
| 49 | - | const buttons = document.querySelectorAll('[id^="start-button-"], [id^="stop-button-"], [id^="remove-button-img-"], [id^="create-container-submit"], [id^="save-button-"]'); | 49 | + | // const xhr = event.detail.xhr; |
| 50 | - | buttons.forEach(button => { | 50 | + | // const statusCode = xhr.status; |
| 51 | - | button.addEventListener('click', function(event) { | 51 | + | |
| 52 | - | setTimeout(function() { | 52 | + | // // Check for 500 status code |
| 53 | - | location.reload(); | 53 | + | // if (statusCode >= 500 && statusCode < 600) { |
| 54 | - | }, 5000); | 54 | + | // // Handle the error (e.g., log it or show an alert) |
| 55 | - | }); | 55 | + | // console.error("Server returned error: ", statusCode); |
| 56 | - | }); | 56 | + | // return; // Do not reload the page |
| 57 | - | });⏎̸ | 57 | + | // } |
| 58 | + | ||||
| 59 | + | // const buttons = document.querySelectorAll('[id^="start-button-"], [id^="stop-button-"], [id^="remove-button-img-"], [id^="create-container-submit"], [id^="save-button-"]'); | |||
| 60 | + | // buttons.forEach(button => { | |||
| 61 | + | // button.addEventListener('click', function(event) { | |||
| 62 | + | // setTimeout(function() { | |||
| 63 | + | // location.reload(); | |||
| 64 | + | // }, 5000); | |||
| 65 | + | // }); | |||
| 66 | + | // }); | |||
| 67 | + | // }); | |||
M pkg/docker/containers.go +70 -28
| @@ -3,6 +3,7 @@ package docker | |||
| 3 | 3 | import ( | |
| 4 | 4 | "bytes" | |
| 5 | 5 | "context" | |
| 6 | + | "fmt" | |
| 6 | 7 | "strings" | |
| 7 | 8 | "time" | |
| 8 | 9 | ||
| @@ -21,7 +22,7 @@ type ContainerCommandParams struct { | |||
| 21 | 22 | EnvVar string | |
| 22 | 23 | ImageName string | |
| 23 | 24 | ImageID string | |
| 24 | - | Ports string | |
| 25 | + | Ports []string | |
| 25 | 26 | Volumes []string | |
| 26 | 27 | Labels []string | |
| 27 | 28 | Network string | |
| @@ -29,6 +30,17 @@ type ContainerCommandParams struct { | |||
| 29 | 30 | Environment []string | |
| 30 | 31 | } | |
| 31 | 32 | ||
| 33 | + | func ContainerCommandParamsToConfig(cmdParams ContainerCommandParams) (*container.Config, error) { | |
| 34 | + | return &container.Config{ | |
| 35 | + | Image: cmdParams.ImageName, | |
| 36 | + | Hostname: cmdParams.ContainerHost, | |
| 37 | + | ExposedPorts: map[nat.Port]struct{}{}, | |
| 38 | + | Env: cmdParams.Environment, | |
| 39 | + | Labels: map[string]string{}, | |
| 40 | + | Volumes: map[string]struct{}{}, | |
| 41 | + | }, nil | |
| 42 | + | } | |
| 43 | + | ||
| 32 | 44 | // ListRunningContainers lists all running containers | |
| 33 | 45 | func ListRunningContainers() ([]types.Container, error) { | |
| 34 | 46 | // Check if the Docker client has been initialized | |
| @@ -41,6 +53,13 @@ func ListRunningContainers() ([]types.Container, error) { | |||
| 41 | 53 | return containers, nil | |
| 42 | 54 | } | |
| 43 | 55 | ||
| 56 | + | // StopContainer try to stop a container gracefully, if it fails, it will stop it forcefully | |
| 57 | + | func StopContainer(containerID string) error { | |
| 58 | + | StopContainerGracefully(containerID, 3*time.Second) | |
| 59 | + | StopContainerRagefully(containerID) | |
| 60 | + | return nil | |
| 61 | + | } | |
| 62 | + | ||
| 44 | 63 | // StopContainerGracefully stops a container by sending a SIGTERM and waiting for it to stop | |
| 45 | 64 | func StopContainerGracefully(containerID string, timeoutDuration time.Duration) (bool, error) { | |
| 46 | 65 | ||
| @@ -85,10 +104,19 @@ func StopContainerRagefully(containerID string) error { | |||
| 85 | 104 | return nil | |
| 86 | 105 | } | |
| 87 | 106 | ||
| 88 | - | // DeleteContainer deletes a container | |
| 89 | - | func DeleteContainer(containerID string) error { | |
| 107 | + | // RenameContainer renames a container with the given name | |
| 108 | + | func RenameContainer(containerID string, newName string) error { | |
| 109 | + | // Rename container using the Docker client | |
| 110 | + | err := dockerCli.ContainerRename(context.Background(), containerID, newName) | |
| 111 | + | if err != nil { | |
| 112 | + | return err | |
| 113 | + | } | |
| 90 | 114 | ||
| 91 | - | // Delete container using the Docker client | |
| 115 | + | return nil | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | // RemoveContainer | |
| 119 | + | func RemoveContainer(containerID string) error { | |
| 92 | 120 | err := dockerCli.ContainerRemove(context.Background(), containerID, types.ContainerRemoveOptions{}) | |
| 93 | 121 | if err != nil { | |
| 94 | 122 | return err | |
| @@ -99,29 +127,32 @@ func DeleteContainer(containerID string) error { | |||
| 99 | 127 | ||
| 100 | 128 | // StartContainer starts a container | |
| 101 | 129 | func StartContainer(containerID string) error { | |
| 130 | + | fmt.Println("Starting container", containerID) | |
| 131 | + | // Check if the container is not already in a running state | |
| 132 | + | containerInfo, err := GetContainerInfo(containerID) | |
| 133 | + | if err != nil { | |
| 134 | + | return fmt.Errorf("could not get container info: %v", err) | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | if containerInfo.State.Running { | |
| 138 | + | return fmt.Errorf("container is already running") | |
| 139 | + | } | |
| 102 | 140 | ||
| 103 | 141 | // Start container using the Docker client | |
| 104 | - | err := dockerCli.ContainerStart(context.Background(), containerID, types.ContainerStartOptions{}) | |
| 142 | + | err = dockerCli.ContainerStart(context.Background(), containerID, types.ContainerStartOptions{}) | |
| 105 | 143 | if err != nil { | |
| 106 | - | return err | |
| 144 | + | return fmt.Errorf("could not start container: %v", err) | |
| 107 | 145 | } | |
| 108 | 146 | ||
| 109 | 147 | return nil | |
| 110 | 148 | } | |
| 111 | 149 | ||
| 112 | 150 | // CreateContainer creates a container with the given parameters | |
| 113 | - | func CreateContainer(cmdParams ContainerCommandParams) error { | |
| 151 | + | func CreateContainer(cmdParams ContainerCommandParams) (string, error) { | |
| 114 | 152 | // Check if the Docker client has been initialized | |
| 115 | 153 | CheckIfInitialized() | |
| 116 | 154 | ||
| 117 | - | // Prepare port bindings | |
| 118 | - | portBindings := nat.PortMap{} | |
| 119 | - | parts := strings.Split(cmdParams.Ports, ":") | |
| 120 | - | if len(parts) == 2 { | |
| 121 | - | portBindings[nat.Port(parts[1])] = []nat.PortBinding{ | |
| 122 | - | {HostIP: "0.0.0.0", HostPort: parts[0]}, | |
| 123 | - | } | |
| 124 | - | } | |
| 155 | + | portBindings := map[nat.Port][]nat.PortBinding{} | |
| 125 | 156 | ||
| 126 | 157 | // Prepare labels for Traefik | |
| 127 | 158 | labels := map[string]string{} | |
| @@ -159,20 +190,10 @@ func CreateContainer(cmdParams ContainerCommandParams) error { | |||
| 159 | 190 | cmdParams.ContainerName, | |
| 160 | 191 | ) | |
| 161 | 192 | if err != nil { | |
| 162 | - | return err | |
| 163 | - | } | |
| 164 | - | ||
| 165 | - | // Start the container | |
| 166 | - | err = dockerCli.ContainerStart( | |
| 167 | - | context.Background(), | |
| 168 | - | resp.ID, | |
| 169 | - | types.ContainerStartOptions{}, | |
| 170 | - | ) | |
| 171 | - | if err != nil { | |
| 172 | - | return err | |
| 193 | + | return "", err | |
| 173 | 194 | } | |
| 174 | 195 | ||
| 175 | - | return nil | |
| 196 | + | return resp.ID, nil | |
| 176 | 197 | } | |
| 177 | 198 | ||
| 178 | 199 | // GetContainerInfo returns information about a container | |
| @@ -188,7 +209,7 @@ func GetContainerInfo(containerID string) (types.ContainerJSON, error) { | |||
| 188 | 209 | } | |
| 189 | 210 | ||
| 190 | 211 | // UpdateContainerConfig updates the configuration of an existing container. | |
| 191 | - | func UpdateContainerConfig(containerID string, newConfig *container.Config) error { | |
| 212 | + | func UpdateContainerConfig(containerID string, newConfig *container.Config, newHostConfig *container.HostConfig, newNetworkingConfig *network.NetworkingConfig) error { | |
| 192 | 213 | ctx := context.Background() | |
| 193 | 214 | // 1. Gracefully stop the existing container | |
| 194 | 215 | _, err := StopContainerGracefully(containerID, 3*time.Second) | |
| @@ -238,3 +259,24 @@ func GetContainerLogs(containerID string) (string, error) { | |||
| 238 | 259 | ||
| 239 | 260 | return containerLogsString, nil | |
| 240 | 261 | } | |
| 262 | + | ||
| 263 | + | // CheckContainerStatus checks if a container with the given name exists and is running | |
| 264 | + | func CheckContainerStatus(containerName string) (bool, bool, error) { | |
| 265 | + | ||
| 266 | + | // List all containers | |
| 267 | + | containers, err := ListRunningContainers() | |
| 268 | + | if err != nil { | |
| 269 | + | return false, false, err | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | // Check if a container with the given name exists and is running | |
| 273 | + | for _, container := range containers { | |
| 274 | + | for _, name := range container.Names { | |
| 275 | + | if strings.TrimLeft(name, "/") == containerName { | |
| 276 | + | return true, container.State == "running", nil | |
| 277 | + | } | |
| 278 | + | } | |
| 279 | + | } | |
| 280 | + | ||
| 281 | + | return false, false, nil | |
| 282 | + | } | |
| @@ -3,6 +3,7 @@ package docker | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "bytes" | 4 | "bytes" | ||
| 5 | "context" | 5 | "context" | ||
| 6 | + | "fmt" | |||
| 6 | "strings" | 7 | "strings" | ||
| 7 | "time" | 8 | "time" | ||
| 8 | 9 | ||||
| @@ -21,7 +22,7 @@ type ContainerCommandParams struct { | |||||
| 21 | EnvVar string | 22 | EnvVar string | ||
| 22 | ImageName string | 23 | ImageName string | ||
| 23 | ImageID string | 24 | ImageID string | ||
| 24 | - | Ports string | 25 | + | Ports []string |
| 25 | Volumes []string | 26 | Volumes []string | ||
| 26 | Labels []string | 27 | Labels []string | ||
| 27 | Network string | 28 | Network string | ||
| @@ -29,6 +30,17 @@ type ContainerCommandParams struct { | |||||
| 29 | Environment []string | 30 | Environment []string | ||
| 30 | } | 31 | } | ||
| 31 | 32 | ||||
| 33 | + | func ContainerCommandParamsToConfig(cmdParams ContainerCommandParams) (*container.Config, error) { | |||
| 34 | + | return &container.Config{ | |||
| 35 | + | Image: cmdParams.ImageName, | |||
| 36 | + | Hostname: cmdParams.ContainerHost, | |||
| 37 | + | ExposedPorts: map[nat.Port]struct{}{}, | |||
| 38 | + | Env: cmdParams.Environment, | |||
| 39 | + | Labels: map[string]string{}, | |||
| 40 | + | Volumes: map[string]struct{}{}, | |||
| 41 | + | }, nil | |||
| 42 | + | } | |||
| 43 | + | ||||
| 32 | // ListRunningContainers lists all running containers | 44 | // ListRunningContainers lists all running containers | ||
| 33 | func ListRunningContainers() ([]types.Container, error) { | 45 | func ListRunningContainers() ([]types.Container, error) { | ||
| 34 | // Check if the Docker client has been initialized | 46 | // Check if the Docker client has been initialized | ||
| @@ -41,6 +53,13 @@ func ListRunningContainers() ([]types.Container, error) { | |||||
| 41 | return containers, nil | 53 | return containers, nil | ||
| 42 | } | 54 | } | ||
| 43 | 55 | ||||
| 56 | + | // StopContainer try to stop a container gracefully, if it fails, it will stop it forcefully | |||
| 57 | + | func StopContainer(containerID string) error { | |||
| 58 | + | StopContainerGracefully(containerID, 3*time.Second) | |||
| 59 | + | StopContainerRagefully(containerID) | |||
| 60 | + | return nil | |||
| 61 | + | } | |||
| 62 | + | ||||
| 44 | // StopContainerGracefully stops a container by sending a SIGTERM and waiting for it to stop | 63 | // StopContainerGracefully stops a container by sending a SIGTERM and waiting for it to stop | ||
| 45 | func StopContainerGracefully(containerID string, timeoutDuration time.Duration) (bool, error) { | 64 | func StopContainerGracefully(containerID string, timeoutDuration time.Duration) (bool, error) { | ||
| 46 | 65 | ||||
| @@ -85,10 +104,19 @@ func StopContainerRagefully(containerID string) error { | |||||
| 85 | return nil | 104 | return nil | ||
| 86 | } | 105 | } | ||
| 87 | 106 | ||||
| 88 | - | // DeleteContainer deletes a container | 107 | + | // RenameContainer renames a container with the given name |
| 89 | - | func DeleteContainer(containerID string) error { | 108 | + | func RenameContainer(containerID string, newName string) error { |
| 109 | + | // Rename container using the Docker client | |||
| 110 | + | err := dockerCli.ContainerRename(context.Background(), containerID, newName) | |||
| 111 | + | if err != nil { | |||
| 112 | + | return err | |||
| 113 | + | } | |||
| 90 | 114 | ||||
| 91 | - | // Delete container using the Docker client | 115 | + | return nil |
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | // RemoveContainer | |||
| 119 | + | func RemoveContainer(containerID string) error { | |||
| 92 | err := dockerCli.ContainerRemove(context.Background(), containerID, types.ContainerRemoveOptions{}) | 120 | err := dockerCli.ContainerRemove(context.Background(), containerID, types.ContainerRemoveOptions{}) | ||
| 93 | if err != nil { | 121 | if err != nil { | ||
| 94 | return err | 122 | return err | ||
| @@ -99,29 +127,32 @@ func DeleteContainer(containerID string) error { | |||||
| 99 | 127 | ||||
| 100 | // StartContainer starts a container | 128 | // StartContainer starts a container | ||
| 101 | func StartContainer(containerID string) error { | 129 | func StartContainer(containerID string) error { | ||
| 130 | + | fmt.Println("Starting container", containerID) | |||
| 131 | + | // Check if the container is not already in a running state | |||
| 132 | + | containerInfo, err := GetContainerInfo(containerID) | |||
| 133 | + | if err != nil { | |||
| 134 | + | return fmt.Errorf("could not get container info: %v", err) | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | if containerInfo.State.Running { | |||
| 138 | + | return fmt.Errorf("container is already running") | |||
| 139 | + | } | |||
| 102 | 140 | ||||
| 103 | // Start container using the Docker client | 141 | // Start container using the Docker client | ||
| 104 | - | err := dockerCli.ContainerStart(context.Background(), containerID, types.ContainerStartOptions{}) | 142 | + | err = dockerCli.ContainerStart(context.Background(), containerID, types.ContainerStartOptions{}) |
| 105 | if err != nil { | 143 | if err != nil { | ||
| 106 | - | return err | 144 | + | return fmt.Errorf("could not start container: %v", err) |
| 107 | } | 145 | } | ||
| 108 | 146 | ||||
| 109 | return nil | 147 | return nil | ||
| 110 | } | 148 | } | ||
| 111 | 149 | ||||
| 112 | // CreateContainer creates a container with the given parameters | 150 | // CreateContainer creates a container with the given parameters | ||
| 113 | - | func CreateContainer(cmdParams ContainerCommandParams) error { | 151 | + | func CreateContainer(cmdParams ContainerCommandParams) (string, error) { |
| 114 | // Check if the Docker client has been initialized | 152 | // Check if the Docker client has been initialized | ||
| 115 | CheckIfInitialized() | 153 | CheckIfInitialized() | ||
| 116 | 154 | ||||
| 117 | - | // Prepare port bindings | 155 | + | portBindings := map[nat.Port][]nat.PortBinding{} |
| 118 | - | portBindings := nat.PortMap{} | |||
| 119 | - | parts := strings.Split(cmdParams.Ports, ":") | |||
| 120 | - | if len(parts) == 2 { | |||
| 121 | - | portBindings[nat.Port(parts[1])] = []nat.PortBinding{ | |||
| 122 | - | {HostIP: "0.0.0.0", HostPort: parts[0]}, | |||
| 123 | - | } | |||
| 124 | - | } | |||
| 125 | 156 | ||||
| 126 | // Prepare labels for Traefik | 157 | // Prepare labels for Traefik | ||
| 127 | labels := map[string]string{} | 158 | labels := map[string]string{} | ||
| @@ -159,20 +190,10 @@ func CreateContainer(cmdParams ContainerCommandParams) error { | |||||
| 159 | cmdParams.ContainerName, | 190 | cmdParams.ContainerName, | ||
| 160 | ) | 191 | ) | ||
| 161 | if err != nil { | 192 | if err != nil { | ||
| 162 | - | return err | 193 | + | return "", err |
| 163 | - | } | |||
| 164 | - | ||||
| 165 | - | // Start the container | |||
| 166 | - | err = dockerCli.ContainerStart( | |||
| 167 | - | context.Background(), | |||
| 168 | - | resp.ID, | |||
| 169 | - | types.ContainerStartOptions{}, | |||
| 170 | - | ) | |||
| 171 | - | if err != nil { | |||
| 172 | - | return err | |||
| 173 | } | 194 | } | ||
| 174 | 195 | ||||
| 175 | - | return nil | 196 | + | return resp.ID, nil |
| 176 | } | 197 | } | ||
| 177 | 198 | ||||
| 178 | // GetContainerInfo returns information about a container | 199 | // GetContainerInfo returns information about a container | ||
| @@ -188,7 +209,7 @@ func GetContainerInfo(containerID string) (types.ContainerJSON, error) { | |||||
| 188 | } | 209 | } | ||
| 189 | 210 | ||||
| 190 | // UpdateContainerConfig updates the configuration of an existing container. | 211 | // UpdateContainerConfig updates the configuration of an existing container. | ||
| 191 | - | func UpdateContainerConfig(containerID string, newConfig *container.Config) error { | 212 | + | func UpdateContainerConfig(containerID string, newConfig *container.Config, newHostConfig *container.HostConfig, newNetworkingConfig *network.NetworkingConfig) error { |
| 192 | ctx := context.Background() | 213 | ctx := context.Background() | ||
| 193 | // 1. Gracefully stop the existing container | 214 | // 1. Gracefully stop the existing container | ||
| 194 | _, err := StopContainerGracefully(containerID, 3*time.Second) | 215 | _, err := StopContainerGracefully(containerID, 3*time.Second) | ||
| @@ -238,3 +259,24 @@ func GetContainerLogs(containerID string) (string, error) { | |||||
| 238 | 259 | ||||
| 239 | return containerLogsString, nil | 260 | return containerLogsString, nil | ||
| 240 | } | 261 | } | ||
| 262 | + | ||||
| 263 | + | // CheckContainerStatus checks if a container with the given name exists and is running | |||
| 264 | + | func CheckContainerStatus(containerName string) (bool, bool, error) { | |||
| 265 | + | ||||
| 266 | + | // List all containers | |||
| 267 | + | containers, err := ListRunningContainers() | |||
| 268 | + | if err != nil { | |||
| 269 | + | return false, false, err | |||
| 270 | + | } | |||
| 271 | + | ||||
| 272 | + | // Check if a container with the given name exists and is running | |||
| 273 | + | for _, container := range containers { | |||
| 274 | + | for _, name := range container.Names { | |||
| 275 | + | if strings.TrimLeft(name, "/") == containerName { | |||
| 276 | + | return true, container.State == "running", nil | |||
| 277 | + | } | |||
| 278 | + | } | |||
| 279 | + | } | |||
| 280 | + | ||||
| 281 | + | return false, false, nil | |||
| 282 | + | } | |||
M pkg/docker/images.go +27 -0
| @@ -30,6 +30,33 @@ func DeleteContainerImage(imageID string) error { | |||
| 30 | 30 | return nil | |
| 31 | 31 | } | |
| 32 | 32 | ||
| 33 | + | func GetImageID(imageName string) (string, error) { | |
| 34 | + | // Check if the Docker client has been initialized | |
| 35 | + | CheckIfInitialized() | |
| 36 | + | ||
| 37 | + | // List all images | |
| 38 | + | images, err := dockerCli.ImageList(context.Background(), types.ImageListOptions{}) | |
| 39 | + | if err != nil { | |
| 40 | + | return "", err | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // Search for the image we just loaded | |
| 44 | + | var imageID string | |
| 45 | + | for _, image := range images { | |
| 46 | + | for _, tag := range image.RepoTags { | |
| 47 | + | if tag == imageName { | |
| 48 | + | imageID = image.ID | |
| 49 | + | } | |
| 50 | + | } | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | if imageID == "" { | |
| 54 | + | return "", err | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | return imageID, nil | |
| 58 | + | } | |
| 59 | + | ||
| 33 | 60 | // ImportImageToEngine imports an image to the Docker engine | |
| 34 | 61 | func ImportImageToEngine(imagePath string) (string, error) { | |
| 35 | 62 | // Open the image file | |
| @@ -30,6 +30,33 @@ func DeleteContainerImage(imageID string) error { | |||||
| 30 | return nil | 30 | return nil | ||
| 31 | } | 31 | } | ||
| 32 | 32 | ||||
| 33 | + | func GetImageID(imageName string) (string, error) { | |||
| 34 | + | // Check if the Docker client has been initialized | |||
| 35 | + | CheckIfInitialized() | |||
| 36 | + | ||||
| 37 | + | // List all images | |||
| 38 | + | images, err := dockerCli.ImageList(context.Background(), types.ImageListOptions{}) | |||
| 39 | + | if err != nil { | |||
| 40 | + | return "", err | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // Search for the image we just loaded | |||
| 44 | + | var imageID string | |||
| 45 | + | for _, image := range images { | |||
| 46 | + | for _, tag := range image.RepoTags { | |||
| 47 | + | if tag == imageName { | |||
| 48 | + | imageID = image.ID | |||
| 49 | + | } | |||
| 50 | + | } | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | if imageID == "" { | |||
| 54 | + | return "", err | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | return imageID, nil | |||
| 58 | + | } | |||
| 59 | + | ||||
| 33 | // ImportImageToEngine imports an image to the Docker engine | 60 | // ImportImageToEngine imports an image to the Docker engine | ||
| 34 | func ImportImageToEngine(imagePath string) (string, error) { | 61 | func ImportImageToEngine(imagePath string) (string, error) { | ||
| 35 | // Open the image file | 62 | // Open the image file | ||