detached 608b713

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
608b713081393c2441f5d8ea08e9313daaae8860
author
bnema · · 2023-10-06 15:13 UTC
parent
269d8af
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 {
165165 // ContainerManagerDelete handles the /container-manager/delete route
166166 func ContainerManagerDelete(c echo.Context, a *app.App) error {
167167 ID := c.Param("ID")
168- err := docker.DeleteContainer(ID)
168+ err := docker.RemoveContainer(ID)
169169 if err != nil {
170170 return sendError(c, err)
171171 }
@@ -165,7 +165,7 @@ func ContainerManagerComponent(c echo.Context, a *app.App) error {
165 // ContainerManagerDelete handles the /container-manager/delete route165 // 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) {
1818 idMapMutex.Unlock()
1919
2020 if !exists {
21- return "", fmt.Errorf("Invalid ShortImgID")
21+ return "", fmt.Errorf("image ID not found")
2222 }
2323
2424 return imageID, nil
@@ -73,7 +73,6 @@ func CreateContainerGET(c echo.Context, a *app.App) error {
7373 func CreateContainerPOST(c echo.Context, a *app.App) error {
7474 // Retreive the ShortID of the image from the URL
7575 ShortID := c.Param("ID")
76- fmt.Println(ShortID)
7776
7877 // Convert the ShortID to a full image ID
7978 imageID, err := FromShortIDToImageID(ShortID)
@@ -113,7 +112,7 @@ func CreateContainerPOST(c echo.Context, a *app.App) error {
113112 }
114113
115114 // Create the container
116- err = docker.CreateContainer(cmdParams)
115+ _, err = docker.CreateContainer(cmdParams)
117116 if err != nil {
118117 return sendError(c, err)
119118 }
@@ -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, nil24 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 URL74 // 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 ID77 // 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 container114 // 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
22
33 import (
44 "fmt"
5+ "log"
56 "strings"
67
78 "github.com/bnema/gordon/internal/app"
@@ -36,8 +37,6 @@ func generateOrderedYAML(infoMap map[string]interface{}) (string, error) {
3637 func ContainerManagerEditGET(c echo.Context, a *app.App) error {
3738 containerID := c.Param("ID")
3839
39- fmt.Println(containerID)
40-
4140 // Get the container info
4241 containerInfo, err := docker.GetContainerInfo(containerID)
4342 if err != nil {
@@ -89,6 +88,7 @@ func ContainerManagerEditGET(c echo.Context, a *app.App) error {
8988 // Pass the containerInfo to the data map
9089 data := map[string]interface{}{
9190 "Title": "Edit container",
91+ "ID": containerID,
9292 "ContainerInfo": yamlString,
9393 }
9494
@@ -107,16 +107,196 @@ func ContainerManagerEditGET(c echo.Context, a *app.App) error {
107107 return c.HTML(200, renderedHTML)
108108 }
109109
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
114112
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+}
117122
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+}
119127
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+ }
120148 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))
121186
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+ }
122302 }
@@ -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 info40 // 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 map88 // 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 container110+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 config113+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 nil148 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+>
26 <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+ >
414 <textarea
515 name="container_config"
616 id="container_config"
@@ -12,8 +22,9 @@
1222 type="submit"
1323 value="Save"
1424 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"
1626 />
1727 </form>
28+ <div id="error-message-edit-container-{{.ID}}" class="text-red-500"></div>
1829 </div>
1930 </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 <textarea14 <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
1919
2020 fmt.Println("environmentSlice", environmentSlice)
2121
22+ portsStr := inputs["ports"]
23+
2224 params := docker.ContainerCommandParams{
2325 IsSSL: inputs["container_protocol"] == "https",
2426 ContainerName: inputs["container_name"],
@@ -26,7 +28,7 @@ func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandPar
2628 Domain: inputs["container_domain"],
2729 ImageName: inputs["image_name"],
2830 ImageID: inputs["image_id"],
29- Ports: inputs["ports"],
31+ Ports: strings.Split(portsStr, ","),
3032 Restart: inputs["restart"],
3133 Volumes: volumeSlice,
3234 Environment: environmentSlice,
@@ -34,7 +36,7 @@ func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandPar
3436 }
3537
3638 // 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]
3840
3941 entryPoint := "web"
4042 if params.IsSSL {
@@ -57,3 +59,49 @@ func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandPar
5759
5860 return params, nil
5961 }
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 balancer38 // 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, nil60 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 {
689689 table-layout: auto;
690690 }
691691
692-.table-fixed {
693- table-layout: fixed;
692+.cursor-pointer {
693+ cursor: pointer;
694694 }
695695
696696 .flex-row {
@@ -791,11 +791,6 @@ video {
791791 background-color: rgb(0 123 192 / var(--tw-bg-opacity));
792792 }
793793
794-.bg-gordonmascot-mblue {
795- --tw-bg-opacity: 1;
796- background-color: rgb(0 82 147 / var(--tw-bg-opacity));
797-}
798-
799794 .bg-gray-100 {
800795 --tw-bg-opacity: 1;
801796 background-color: rgb(243 244 246 / var(--tw-bg-opacity));
@@ -852,6 +847,11 @@ video {
852847 padding-right: 1rem;
853848 }
854849
850+.px-8 {
851+ padding-left: 2rem;
852+ padding-right: 2rem;
853+}
854+
855855 .py-1 {
856856 padding-top: 0.25rem;
857857 padding-bottom: 0.25rem;
@@ -866,10 +866,6 @@ video {
866866 padding-bottom: 1rem;
867867 }
868868
869-.text-left {
870- text-align: left;
871-}
872-
873869 .text-center {
874870 text-align: center;
875871 }
@@ -917,10 +913,6 @@ video {
917913 font-weight: 700;
918914 }
919915
920-.font-light {
921- font-weight: 300;
922-}
923-
924916 .font-semibold {
925917 font-weight: 600;
926918 }
@@ -1130,9 +1122,9 @@ video {
11301122 background-color: rgb(29 78 216 / var(--tw-bg-opacity));
11311123 }
11321124
1133-.hover\:bg-gordonmascot-hblue:hover {
1125+.hover\:bg-gordonmascot-mblue:hover {
11341126 --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));
11361128 }
11371129
11381130 @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) {
4444 });
4545 });
4646
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
33 import (
44 "bytes"
55 "context"
6+ "fmt"
67 "strings"
78 "time"
89
@@ -21,7 +22,7 @@ type ContainerCommandParams struct {
2122 EnvVar string
2223 ImageName string
2324 ImageID string
24- Ports string
25+ Ports []string
2526 Volumes []string
2627 Labels []string
2728 Network string
@@ -29,6 +30,17 @@ type ContainerCommandParams struct {
2930 Environment []string
3031 }
3132
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+
3244 // ListRunningContainers lists all running containers
3345 func ListRunningContainers() ([]types.Container, error) {
3446 // Check if the Docker client has been initialized
@@ -41,6 +53,13 @@ func ListRunningContainers() ([]types.Container, error) {
4153 return containers, nil
4254 }
4355
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+
4463 // StopContainerGracefully stops a container by sending a SIGTERM and waiting for it to stop
4564 func StopContainerGracefully(containerID string, timeoutDuration time.Duration) (bool, error) {
4665
@@ -85,10 +104,19 @@ func StopContainerRagefully(containerID string) error {
85104 return nil
86105 }
87106
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+ }
90114
91- // Delete container using the Docker client
115+ return nil
116+}
117+
118+// RemoveContainer
119+func RemoveContainer(containerID string) error {
92120 err := dockerCli.ContainerRemove(context.Background(), containerID, types.ContainerRemoveOptions{})
93121 if err != nil {
94122 return err
@@ -99,29 +127,32 @@ func DeleteContainer(containerID string) error {
99127
100128 // StartContainer starts a container
101129 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+ }
102140
103141 // Start container using the Docker client
104- err := dockerCli.ContainerStart(context.Background(), containerID, types.ContainerStartOptions{})
142+ err = dockerCli.ContainerStart(context.Background(), containerID, types.ContainerStartOptions{})
105143 if err != nil {
106- return err
144+ return fmt.Errorf("could not start container: %v", err)
107145 }
108146
109147 return nil
110148 }
111149
112150 // CreateContainer creates a container with the given parameters
113-func CreateContainer(cmdParams ContainerCommandParams) error {
151+func CreateContainer(cmdParams ContainerCommandParams) (string, error) {
114152 // Check if the Docker client has been initialized
115153 CheckIfInitialized()
116154
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{}
125156
126157 // Prepare labels for Traefik
127158 labels := map[string]string{}
@@ -159,20 +190,10 @@ func CreateContainer(cmdParams ContainerCommandParams) error {
159190 cmdParams.ContainerName,
160191 )
161192 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
173194 }
174195
175- return nil
196+ return resp.ID, nil
176197 }
177198
178199 // GetContainerInfo returns information about a container
@@ -188,7 +209,7 @@ func GetContainerInfo(containerID string) (types.ContainerJSON, error) {
188209 }
189210
190211 // 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 {
192213 ctx := context.Background()
193214 // 1. Gracefully stop the existing container
194215 _, err := StopContainerGracefully(containerID, 3*time.Second)
@@ -238,3 +259,24 @@ func GetContainerLogs(containerID string) (string, error) {
238259
239260 return containerLogsString, nil
240261 }
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 string22 EnvVar string
22 ImageName string23 ImageName string
23 ImageID string24 ImageID string
24- Ports string25+ Ports []string
25 Volumes []string26 Volumes []string
26 Labels []string27 Labels []string
27 Network string28 Network string
@@ -29,6 +30,17 @@ type ContainerCommandParams struct {
29 Environment []string30 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 containers44 // 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 initialized46 // Check if the Docker client has been initialized
@@ -41,6 +53,13 @@ func ListRunningContainers() ([]types.Container, error) {
41 return containers, nil53 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 stop63 // 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 nil104 return nil
86 }105 }
87 106
88-// DeleteContainer deletes a container107+// 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 client115+ 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 err122 return err
@@ -99,29 +127,32 @@ func DeleteContainer(containerID string) error {
99 127
100 // StartContainer starts a container128 // 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 client141 // 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 err144+ return fmt.Errorf("could not start container: %v", err)
107 }145 }
108 146
109 return nil147 return nil
110 }148 }
111 149
112 // CreateContainer creates a container with the given parameters150 // 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 initialized152 // Check if the Docker client has been initialized
115 CheckIfInitialized()153 CheckIfInitialized()
116 154
117- // Prepare port bindings155+ 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 Traefik157 // 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 err193+ 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 nil196+ return resp.ID, nil
176 }197 }
177 198
178 // GetContainerInfo returns information about a container199 // 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 container214 // 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, nil260 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 {
3030 return nil
3131 }
3232
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+
3360 // ImportImageToEngine imports an image to the Docker engine
3461 func ImportImageToEngine(imagePath string) (string, error) {
3562 // Open the image file
@@ -30,6 +30,33 @@ func DeleteContainerImage(imageID string) error {
30 return nil30 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 engine60 // 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 file62 // Open the image file

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close