detached 63805d6

Enhanced create container form. Some Logic refactoring on the router.

commit
63805d6b67d3a00793b32826001f35a745a773e9
author
bnema · · 2023-09-26 17:35 UTC
parent
7c6200e
signature
unsigned
8 files · +326 -125
M internal/gotemplate/models/html/fragments/createcontainer.gohtml +86 -23
@@ -1,5 +1,5 @@
11 <div
2- class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow"
2+ class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" hx-ext="response-targets"
33 >
44 <!-- Left Side: Docker Run Options -->
55 <div class="flex-grow bg-slate-100 p-4 rounded-md">
@@ -12,20 +12,35 @@
1212 method="post"
1313 hx-post="/htmx/create-container/{{.ShortID}}"
1414 hx-swap="innerHTML"
15+ hx-target-*="#error-message-create-container-{{.ShortID}}"
1516 >
1617 <!-- Image Name -->
17- <div class="mb-4">
18- <label class="block">Image name</label>
19- <input
20- type="text"
21- name="image_name"
22- class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2"
23- value="{{.ImageName}}"
24- readonly
25- />
18+ <div class="flex space-x-4 pb-4">
19+ <div class="w-1/3">
20+ <label class="block">Image name</label>
21+ <input
22+ type="text"
23+ name="image_name"
24+ class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2"
25+ value="{{.ImageName}}"
26+ readonly
27+ />
28+ </div>
29+
30+ <div class="w-2/3">
31+ <label class="block">Image ID</label>
32+ <input
33+ type="text"
34+ name="image_id"
35+ class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2"
36+ value="{{.ImageID}}"
37+ readonly
38+ />
39+ </div>
2640 </div>
41+
2742 <!-- Container Name -->
28- <div class="mb-4">
43+ <div class="pb-4">
2944 <label class="block">Container name</label>
3045 <input
3146 type="text"
@@ -34,19 +49,41 @@
3449 value="{{.ContainerName}}"
3550 />
3651 </div>
37- <!-- Domain/Subdomain -->
38- <div class="mb-4">
39- <label class="block">Domain/Subdomain</label>
40- <input
41- type="text"
42- name="container_domain"
43- class="w-full p-4 rounded-md"
44- value="{{.ContainerDomain}}"
45- />
52+ <!-- Http(s) / Subdomain / Domain -->
53+ <div class="flex pb-4 space-x-4">
54+ <!-- Protocol -->
55+ <div class="w-1/4">
56+ <label class="block">Protocol</label>
57+ <select name="container_protocol" class="w-full p-4 rounded-md border-2 bg-slate-200 border-gray-300">
58+ <option value="https" {{if eq .ContainerProtocol "https"}}selected{{end}}>https</option>
59+ <option value="http" {{if eq .ContainerProtocol "http"}}selected{{end}}>http</option>
60+ </select>
61+ </div>
62+
63+ <!-- Subdomain -->
64+ <div class="w-1/3">
65+ <label class="block">Subdomain</label>
66+ <input
67+ type="text"
68+ name="container_subdomain"
69+ class="w-full p-4 rounded-md"
70+ value="{{.ContainerSubdomain}}"
71+ />
72+ </div>
73+
74+ <!-- Domain -->
75+ <div class="w-5/12">
76+ <label class="block">Domain</label>
77+ <input
78+ type="text"
79+ name="container_domain"
80+ class="w-full p-4 rounded-md"
81+ value="{{.ContainerDomain}}"
82+ />
83+ </div>
4684 </div>
47-
4885 <!-- Ports -->
49- <div class="mb-4">
86+ <div class="pb-4">
5087 <label class="block">Ports</label>
5188 <span class="text-gray-500"
5289 >e.g. 80:80 (left is host, right is container)</span
@@ -60,7 +97,7 @@
6097 </div>
6198
6299 <!-- Volumes -->
63- <div class="mb-4">
100+ <div class="pb-4">
64101 <label class="block">Volumes</label>
65102 <span class="text-gray-500">e.g. ./data:/container/data </span>
66103 <input
@@ -71,11 +108,37 @@
71108 />
72109 </div>
73110
111+ <!-- Environment Variables -->
112+ <div class="pb-4">
113+ <label class="block">Environment variables</label>
114+ <span class="text-gray-500">e.g. KEY=VALUE </span>
115+ <input
116+ type="text"
117+ name="environment_variables"
118+ class="w-full p-4 rounded-md"
119+ value="{{.EnvironmentVariables}}"
120+ />
121+
122+ <!-- Restart: always/on-failure/only when stopped/never -->
123+ <div class="pb-4">
124+ <label class="block">Restart</label>
125+ <select
126+ name="restart"
127+ class="w-full p-4 rounded-md border-2 bg-gray-100 border-gray-300"
128+ >
129+ <option value="always">Always</option>
130+ <option value="on-failure">On failure</option>
131+ <option value="unless-stopped">Unless stopped</option>
132+ <option value="never">Never</option>
133+ </select>
134+ </div>
135+
74136 <input
75137 type="submit"
76138 value="Create"
77139 class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
78140 />
79141 </form>
142+ <div id="error-message-create-container-{{.ShortID}}" class="text-red-500"></div>
80143 </div>
81144 </div>
@@ -1,5 +1,5 @@
1 <div1 <div
2- class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow"2+ class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" hx-ext="response-targets"
3 >3 >
4 <!-- Left Side: Docker Run Options -->4 <!-- Left Side: Docker Run Options -->
5 <div class="flex-grow bg-slate-100 p-4 rounded-md">5 <div class="flex-grow bg-slate-100 p-4 rounded-md">
@@ -12,20 +12,35 @@
12 method="post"12 method="post"
13 hx-post="/htmx/create-container/{{.ShortID}}"13 hx-post="/htmx/create-container/{{.ShortID}}"
14 hx-swap="innerHTML"14 hx-swap="innerHTML"
15+ hx-target-*="#error-message-create-container-{{.ShortID}}"
15 >16 >
16 <!-- Image Name -->17 <!-- Image Name -->
17- <div class="mb-4">18+ <div class="flex space-x-4 pb-4">
18- <label class="block">Image name</label>19+ <div class="w-1/3">
19- <input20+ <label class="block">Image name</label>
20- type="text"21+ <input
21- name="image_name"22+ type="text"
22- class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2"23+ name="image_name"
23- value="{{.ImageName}}"24+ class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2"
24- readonly25+ value="{{.ImageName}}"
25- />26+ readonly
27+ />
28+ </div>
29+
30+ <div class="w-2/3">
31+ <label class="block">Image ID</label>
32+ <input
33+ type="text"
34+ name="image_id"
35+ class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2"
36+ value="{{.ImageID}}"
37+ readonly
38+ />
39+ </div>
26 </div>40 </div>
41+
27 <!-- Container Name -->42 <!-- Container Name -->
28- <div class="mb-4">43+ <div class="pb-4">
29 <label class="block">Container name</label>44 <label class="block">Container name</label>
30 <input45 <input
31 type="text"46 type="text"
@@ -34,19 +49,41 @@
34 value="{{.ContainerName}}"49 value="{{.ContainerName}}"
35 />50 />
36 </div>51 </div>
37- <!-- Domain/Subdomain -->52+ <!-- Http(s) / Subdomain / Domain -->
38- <div class="mb-4">53+ <div class="flex pb-4 space-x-4">
39- <label class="block">Domain/Subdomain</label>54+ <!-- Protocol -->
40- <input55+ <div class="w-1/4">
41- type="text"56+ <label class="block">Protocol</label>
42- name="container_domain"57+ <select name="container_protocol" class="w-full p-4 rounded-md border-2 bg-slate-200 border-gray-300">
43- class="w-full p-4 rounded-md"58+ <option value="https" {{if eq .ContainerProtocol "https"}}selected{{end}}>https</option>
44- value="{{.ContainerDomain}}"59+ <option value="http" {{if eq .ContainerProtocol "http"}}selected{{end}}>http</option>
45- />60+ </select>
61+ </div>
62+
63+ <!-- Subdomain -->
64+ <div class="w-1/3">
65+ <label class="block">Subdomain</label>
66+ <input
67+ type="text"
68+ name="container_subdomain"
69+ class="w-full p-4 rounded-md"
70+ value="{{.ContainerSubdomain}}"
71+ />
72+ </div>
73+
74+ <!-- Domain -->
75+ <div class="w-5/12">
76+ <label class="block">Domain</label>
77+ <input
78+ type="text"
79+ name="container_domain"
80+ class="w-full p-4 rounded-md"
81+ value="{{.ContainerDomain}}"
82+ />
83+ </div>
46 </div>84 </div>
47-
48 <!-- Ports -->85 <!-- Ports -->
49- <div class="mb-4">86+ <div class="pb-4">
50 <label class="block">Ports</label>87 <label class="block">Ports</label>
51 <span class="text-gray-500"88 <span class="text-gray-500"
52 >e.g. 80:80 (left is host, right is container)</span89 >e.g. 80:80 (left is host, right is container)</span
@@ -60,7 +97,7 @@
60 </div>97 </div>
61 98
62 <!-- Volumes -->99 <!-- Volumes -->
63- <div class="mb-4">100+ <div class="pb-4">
64 <label class="block">Volumes</label>101 <label class="block">Volumes</label>
65 <span class="text-gray-500">e.g. ./data:/container/data </span>102 <span class="text-gray-500">e.g. ./data:/container/data </span>
66 <input103 <input
@@ -71,11 +108,37 @@
71 />108 />
72 </div>109 </div>
73 110
111+ <!-- Environment Variables -->
112+ <div class="pb-4">
113+ <label class="block">Environment variables</label>
114+ <span class="text-gray-500">e.g. KEY=VALUE </span>
115+ <input
116+ type="text"
117+ name="environment_variables"
118+ class="w-full p-4 rounded-md"
119+ value="{{.EnvironmentVariables}}"
120+ />
121+
122+ <!-- Restart: always/on-failure/only when stopped/never -->
123+ <div class="pb-4">
124+ <label class="block">Restart</label>
125+ <select
126+ name="restart"
127+ class="w-full p-4 rounded-md border-2 bg-gray-100 border-gray-300"
128+ >
129+ <option value="always">Always</option>
130+ <option value="on-failure">On failure</option>
131+ <option value="unless-stopped">Unless stopped</option>
132+ <option value="never">Never</option>
133+ </select>
134+ </div>
135+
74 <input136 <input
75 type="submit"137 type="submit"
76 value="Create"138 value="Create"
77 class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"139 class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
78 />140 />
79 </form>141 </form>
142+ <div id="error-message-create-container-{{.ShortID}}" class="text-red-500"></div>
80 </div>143 </div>
81 </div>144 </div>
A internal/gotemplate/render/cmdparams.go +47 -0
@@ -0,0 +1,47 @@
1+package render
2+
3+import (
4+ "fmt"
5+
6+ "github.com/bnema/gordon/pkg/utils/docker"
7+)
8+
9+// FromInputsToCmdParams transforms a map to a ContainerCommandParams struct
10+func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandParams, error) {
11+ fmt.Println("FromInputsToCmdParams:", inputs)
12+ // Populate the ContainerCommandParams struct directly from the map
13+ params := docker.ContainerCommandParams{
14+ Https: inputs["container_protocol"] == "https",
15+ ContainerName: inputs["container_name"],
16+ ContainerHost: inputs["container_subdomain"],
17+ Domain: inputs["container_domain"],
18+ ImageName: inputs["image_name"],
19+ ImageID: inputs["image_id"],
20+ Ports: inputs["ports"],
21+ Restart: inputs["restart"],
22+ Data: inputs["volumes"],
23+ EnvVar: inputs["environment_variables"],
24+ Network: "traefik",
25+ }
26+
27+ entryPoint := "web"
28+ if params.Https {
29+ entryPoint = "websecure"
30+ }
31+
32+ baseRouter := "traefik.http.routers." + params.ContainerName + "-router"
33+ baseService := "traefik.http.services." + params.ContainerName + "-service"
34+
35+ params.TraefikLabels = []string{
36+ "traefik.enable=true",
37+ fmt.Sprintf("%s.rule=Host(`%s.%s`)", baseRouter, params.ContainerHost, params.Domain),
38+ fmt.Sprintf("%s.entrypoints=%s", baseRouter, entryPoint),
39+ fmt.Sprintf("%s.loadbalancer.server.port=%s", baseService, params.Ports),
40+ }
41+
42+ if params.Https {
43+ params.TraefikLabels = append(params.TraefikLabels, fmt.Sprintf("%s.tls.certresolver=letsencrypt", baseRouter))
44+ }
45+
46+ return params, nil
47+}
@@ -0,0 +1,47 @@
1+package render
2+
3+import (
4+ "fmt"
5+
6+ "github.com/bnema/gordon/pkg/utils/docker"
7+)
8+
9+// FromInputsToCmdParams transforms a map to a ContainerCommandParams struct
10+func FromInputsToCmdParams(inputs map[string]string) (docker.ContainerCommandParams, error) {
11+ fmt.Println("FromInputsToCmdParams:", inputs)
12+ // Populate the ContainerCommandParams struct directly from the map
13+ params := docker.ContainerCommandParams{
14+ Https: inputs["container_protocol"] == "https",
15+ ContainerName: inputs["container_name"],
16+ ContainerHost: inputs["container_subdomain"],
17+ Domain: inputs["container_domain"],
18+ ImageName: inputs["image_name"],
19+ ImageID: inputs["image_id"],
20+ Ports: inputs["ports"],
21+ Restart: inputs["restart"],
22+ Data: inputs["volumes"],
23+ EnvVar: inputs["environment_variables"],
24+ Network: "traefik",
25+ }
26+
27+ entryPoint := "web"
28+ if params.Https {
29+ entryPoint = "websecure"
30+ }
31+
32+ baseRouter := "traefik.http.routers." + params.ContainerName + "-router"
33+ baseService := "traefik.http.services." + params.ContainerName + "-service"
34+
35+ params.TraefikLabels = []string{
36+ "traefik.enable=true",
37+ fmt.Sprintf("%s.rule=Host(`%s.%s`)", baseRouter, params.ContainerHost, params.Domain),
38+ fmt.Sprintf("%s.entrypoints=%s", baseRouter, entryPoint),
39+ fmt.Sprintf("%s.loadbalancer.server.port=%s", baseService, params.Ports),
40+ }
41+
42+ if params.Https {
43+ params.TraefikLabels = append(params.TraefikLabels, fmt.Sprintf("%s.tls.certresolver=letsencrypt", baseRouter))
44+ }
45+
46+ return params, nil
47+}
D internal/gotemplate/render/strings.go +0 -16
@@ -1,16 +0,0 @@
1-// Take some inputs and return as full strings
2-package render
3-
4-type CreateContainer struct {
5- ContainerName string
6- ContainerHost string
7- Environment string
8- ImageName string
9- ImageID string
10- Ports string
11- Data string
12- TraefikLabels []string
13- Network string
14-}
15-
16-const dockerContainerTemplate = ""
@@ -1,16 +0,0 @@
1-// Take some inputs and return as full strings
2-package render
3-
4-type CreateContainer struct {
5- ContainerName string
6- ContainerHost string
7- Environment string
8- ImageName string
9- ImageID string
10- Ports string
11- Data string
12- TraefikLabels []string
13- Network string
14-}
15-
16-const dockerContainerTemplate = ""
M internal/httpserve/handler/containers.go +22 -14
@@ -36,8 +36,8 @@ type HumanReadableContainer struct {
3636 StateColor string
3737 }
3838
39-// ImageManagerHandler handles the /image-manager route (HTMX route)
40-func ImageManagerHandler(c echo.Context, a *app.App) error {
39+// ImageManagerComponent handles the /image-manager route (HTMX route)
40+func ImageManagerComponent(c echo.Context, a *app.App) error {
4141 images, err := docker.ListContainerImages()
4242 if err != nil {
4343 return sendError(c, err)
@@ -78,8 +78,8 @@ func ImageManagerHandler(c echo.Context, a *app.App) error {
7878 return c.HTML(200, renderedHTML)
7979 }
8080
81-// ImageManagerDeleteHandler handles the /image-manager/delete route
82-func ImageManagerDeleteHandler(c echo.Context, a *app.App) error {
81+// ImageManagerDelete handles the /image-manager/delete route
82+func ImageManagerDelete(c echo.Context, a *app.App) error {
8383 // Get the ShortImgID from the URL
8484 ShortID := c.Param("ID")
8585
@@ -107,8 +107,8 @@ func ImageManagerDeleteHandler(c echo.Context, a *app.App) error {
107107 return c.HTML(http.StatusOK, `<div>Removed</div>`)
108108 }
109109
110-// ContainerManagerHandler handles the /container-manager route
111-func ContainerManagerHandler(c echo.Context, a *app.App) error {
110+// ContainerManagerComponent handles the /container-manager route
111+func ContainerManagerComponent(c echo.Context, a *app.App) error {
112112 containers, err := docker.ListRunningContainers()
113113 if err != nil {
114114 return sendError(c, err)
@@ -117,8 +117,7 @@ func ContainerManagerHandler(c echo.Context, a *app.App) error {
117117 var humanReadableContainers []HumanReadableContainer
118118
119119 for _, container := range containers {
120- ShortID := container.ID[0:12]
121- idMap[ShortID] = container.ID
120+ localContainer := container // Make a local copy
122121 createdTime := time.Unix(container.Created, 0)
123122 createdStr := humanize.TimeAgo(createdTime)
124123 sizeStr := humanize.BytesToReadableSize(container.SizeRw)
@@ -136,13 +135,12 @@ func ContainerManagerHandler(c echo.Context, a *app.App) error {
136135 for _, name := range container.Names {
137136 name = name[1:]
138137 humanReadableContainers = append(humanReadableContainers, HumanReadableContainer{
139- Container: &container,
138+ Container: &localContainer,
140139 CreatedStr: createdStr,
141140 SizeStr: sizeStr,
142141 UpSince: humanize.TimeAgo(time.Unix(container.Created, 0)),
143142 StateColor: stateColor,
144143 Name: name,
145- ShortID: ShortID,
146144 Ports: ports,
147145 })
148146 }
@@ -162,8 +160,8 @@ func ContainerManagerHandler(c echo.Context, a *app.App) error {
162160 return c.HTML(200, renderedHTML)
163161 }
164162
165-// ContainerManagerDeleteHandler handles the /container-manager/delete route
166-func ContainerManagerDeleteHandler(c echo.Context, a *app.App) error {
163+// ContainerManagerDelete handles the /container-manager/delete route
164+func ContainerManagerDelete(c echo.Context, a *app.App) error {
167165 ID := c.Param("ID")
168166 err := docker.DeleteContainer(ID)
169167 if err != nil {
@@ -172,8 +170,8 @@ func ContainerManagerDeleteHandler(c echo.Context, a *app.App) error {
172170 return c.HTML(http.StatusOK, `Success`)
173171 }
174172
175-// ContainerManagerStopHandler handles the /container-manager/stop route
176-func ContainerManagerStopHandler(c echo.Context, a *app.App) error {
173+// ContainerManagerStop handles the /container-manager/stop route
174+func ContainerManagerStop(c echo.Context, a *app.App) error {
177175 ID := c.Param("ID")
178176 // Stop the container gracefully with a timeout
179177 stopped, err := docker.StopContainerGracefully(ID, 3*time.Second)
@@ -192,3 +190,13 @@ func ContainerManagerStopHandler(c echo.Context, a *app.App) error {
192190
193191 return c.HTML(http.StatusOK, `Success`)
194192 }
193+
194+// ContainerManagerStart handles the /container-manager/start route
195+func ContainerManagerStart(c echo.Context, a *app.App) error {
196+ ID := c.Param("ID")
197+ err := docker.StartContainer(ID)
198+ if err != nil {
199+ return sendError(c, err)
200+ }
201+ return c.HTML(http.StatusOK, `Success`)
202+}
@@ -36,8 +36,8 @@ type HumanReadableContainer struct {
36 StateColor string36 StateColor string
37 }37 }
38 38
39-// ImageManagerHandler handles the /image-manager route (HTMX route)39+// ImageManagerComponent handles the /image-manager route (HTMX route)
40-func ImageManagerHandler(c echo.Context, a *app.App) error {40+func ImageManagerComponent(c echo.Context, a *app.App) error {
41 images, err := docker.ListContainerImages()41 images, err := docker.ListContainerImages()
42 if err != nil {42 if err != nil {
43 return sendError(c, err)43 return sendError(c, err)
@@ -78,8 +78,8 @@ func ImageManagerHandler(c echo.Context, a *app.App) error {
78 return c.HTML(200, renderedHTML)78 return c.HTML(200, renderedHTML)
79 }79 }
80 80
81-// ImageManagerDeleteHandler handles the /image-manager/delete route81+// ImageManagerDelete handles the /image-manager/delete route
82-func ImageManagerDeleteHandler(c echo.Context, a *app.App) error {82+func ImageManagerDelete(c echo.Context, a *app.App) error {
83 // Get the ShortImgID from the URL83 // Get the ShortImgID from the URL
84 ShortID := c.Param("ID")84 ShortID := c.Param("ID")
85 85
@@ -107,8 +107,8 @@ func ImageManagerDeleteHandler(c echo.Context, a *app.App) error {
107 return c.HTML(http.StatusOK, `<div>Removed</div>`)107 return c.HTML(http.StatusOK, `<div>Removed</div>`)
108 }108 }
109 109
110-// ContainerManagerHandler handles the /container-manager route110+// ContainerManagerComponent handles the /container-manager route
111-func ContainerManagerHandler(c echo.Context, a *app.App) error {111+func ContainerManagerComponent(c echo.Context, a *app.App) error {
112 containers, err := docker.ListRunningContainers()112 containers, err := docker.ListRunningContainers()
113 if err != nil {113 if err != nil {
114 return sendError(c, err)114 return sendError(c, err)
@@ -117,8 +117,7 @@ func ContainerManagerHandler(c echo.Context, a *app.App) error {
117 var humanReadableContainers []HumanReadableContainer117 var humanReadableContainers []HumanReadableContainer
118 118
119 for _, container := range containers {119 for _, container := range containers {
120- ShortID := container.ID[0:12]120+ localContainer := container // Make a local copy
121- idMap[ShortID] = container.ID
122 createdTime := time.Unix(container.Created, 0)121 createdTime := time.Unix(container.Created, 0)
123 createdStr := humanize.TimeAgo(createdTime)122 createdStr := humanize.TimeAgo(createdTime)
124 sizeStr := humanize.BytesToReadableSize(container.SizeRw)123 sizeStr := humanize.BytesToReadableSize(container.SizeRw)
@@ -136,13 +135,12 @@ func ContainerManagerHandler(c echo.Context, a *app.App) error {
136 for _, name := range container.Names {135 for _, name := range container.Names {
137 name = name[1:]136 name = name[1:]
138 humanReadableContainers = append(humanReadableContainers, HumanReadableContainer{137 humanReadableContainers = append(humanReadableContainers, HumanReadableContainer{
139- Container: &container,138+ Container: &localContainer,
140 CreatedStr: createdStr,139 CreatedStr: createdStr,
141 SizeStr: sizeStr,140 SizeStr: sizeStr,
142 UpSince: humanize.TimeAgo(time.Unix(container.Created, 0)),141 UpSince: humanize.TimeAgo(time.Unix(container.Created, 0)),
143 StateColor: stateColor,142 StateColor: stateColor,
144 Name: name,143 Name: name,
145- ShortID: ShortID,
146 Ports: ports,144 Ports: ports,
147 })145 })
148 }146 }
@@ -162,8 +160,8 @@ func ContainerManagerHandler(c echo.Context, a *app.App) error {
162 return c.HTML(200, renderedHTML)160 return c.HTML(200, renderedHTML)
163 }161 }
164 162
165-// ContainerManagerDeleteHandler handles the /container-manager/delete route163+// ContainerManagerDelete handles the /container-manager/delete route
166-func ContainerManagerDeleteHandler(c echo.Context, a *app.App) error {164+func ContainerManagerDelete(c echo.Context, a *app.App) error {
167 ID := c.Param("ID")165 ID := c.Param("ID")
168 err := docker.DeleteContainer(ID)166 err := docker.DeleteContainer(ID)
169 if err != nil {167 if err != nil {
@@ -172,8 +170,8 @@ func ContainerManagerDeleteHandler(c echo.Context, a *app.App) error {
172 return c.HTML(http.StatusOK, `Success`)170 return c.HTML(http.StatusOK, `Success`)
173 }171 }
174 172
175-// ContainerManagerStopHandler handles the /container-manager/stop route173+// ContainerManagerStop handles the /container-manager/stop route
176-func ContainerManagerStopHandler(c echo.Context, a *app.App) error {174+func ContainerManagerStop(c echo.Context, a *app.App) error {
177 ID := c.Param("ID")175 ID := c.Param("ID")
178 // Stop the container gracefully with a timeout176 // Stop the container gracefully with a timeout
179 stopped, err := docker.StopContainerGracefully(ID, 3*time.Second)177 stopped, err := docker.StopContainerGracefully(ID, 3*time.Second)
@@ -192,3 +190,13 @@ func ContainerManagerStopHandler(c echo.Context, a *app.App) error {
192 190
193 return c.HTML(http.StatusOK, `Success`)191 return c.HTML(http.StatusOK, `Success`)
194 }192 }
193+
194+// ContainerManagerStart handles the /container-manager/start route
195+func ContainerManagerStart(c echo.Context, a *app.App) error {
196+ ID := c.Param("ID")
197+ err := docker.StartContainer(ID)
198+ if err != nil {
199+ return sendError(c, err)
200+ }
201+ return c.HTML(http.StatusOK, `Success`)
202+}
M internal/httpserve/handler/createcontainer.go +16 -5
@@ -62,6 +62,7 @@ func CreateContainerGET(c echo.Context, a *app.App) error {
6262 data := map[string]interface{}{
6363 "Title": "Create a new container",
6464 "ShortID": ShortID,
65+ "ImageID": imageID,
6566 "ImageName": imageName,
6667 }
6768
@@ -104,7 +105,7 @@ func CreateContainerPOST(c echo.Context, a *app.App) error {
104105 }
105106
106107 // Initialize a map to store sanitized values
107- sanitizedParams := make(map[string]string)
108+ sanitizedInputs := make(map[string]string)
108109
109110 // Iterate over all form parameters and sanitize them
110111 for k, v := range formParams {
@@ -113,12 +114,22 @@ func CreateContainerPOST(c echo.Context, a *app.App) error {
113114 if err != nil {
114115 return sendError(c, err)
115116 }
116- sanitizedParams[k] = sanitized
117+ sanitizedInputs[k] = sanitized
117118 }
118119 }
119120
120- // Print the form values
121- fmt.Printf("Form values: %v\n", sanitizedParams)
121+ // Result : Form values: map[container_domain:gogs.localhost container_name:gogs image_id:sha256:5d92d8ae733d93dc4af5b32bb353539eeff660fc2136c6902abcdf3aafe4cef1 image_name:gogs/gogs:latest ports:8888:80 restart:unless-stopped volumes:/home/data/test:/data]
122+ // With those values we can use gotemplate/txt to create a custom docker run command
123+ cmdParams, err := render.FromInputsToCmdParams(sanitizedInputs) // Make sure cmdParams is of type render.ContainerCommandParams
124+ if err != nil {
125+ return sendError(c, err)
126+ }
127+
128+ // Create the container
129+ err = docker.CreateContainer(cmdParams)
130+ if err != nil {
131+ return sendError(c, err)
132+ }
122133
123- return c.String(200, "OK")
134+ return c.String(200, "Container created")
124135 }
@@ -62,6 +62,7 @@ func CreateContainerGET(c echo.Context, a *app.App) error {
62 data := map[string]interface{}{62 data := map[string]interface{}{
63 "Title": "Create a new container",63 "Title": "Create a new container",
64 "ShortID": ShortID,64 "ShortID": ShortID,
65+ "ImageID": imageID,
65 "ImageName": imageName,66 "ImageName": imageName,
66 }67 }
67 68
@@ -104,7 +105,7 @@ func CreateContainerPOST(c echo.Context, a *app.App) error {
104 }105 }
105 106
106 // Initialize a map to store sanitized values107 // Initialize a map to store sanitized values
107- sanitizedParams := make(map[string]string)108+ sanitizedInputs := make(map[string]string)
108 109
109 // Iterate over all form parameters and sanitize them110 // Iterate over all form parameters and sanitize them
110 for k, v := range formParams {111 for k, v := range formParams {
@@ -113,12 +114,22 @@ func CreateContainerPOST(c echo.Context, a *app.App) error {
113 if err != nil {114 if err != nil {
114 return sendError(c, err)115 return sendError(c, err)
115 }116 }
116- sanitizedParams[k] = sanitized117+ sanitizedInputs[k] = sanitized
117 }118 }
118 }119 }
119 120
120- // Print the form values121+ // Result : Form values: map[container_domain:gogs.localhost container_name:gogs image_id:sha256:5d92d8ae733d93dc4af5b32bb353539eeff660fc2136c6902abcdf3aafe4cef1 image_name:gogs/gogs:latest ports:8888:80 restart:unless-stopped volumes:/home/data/test:/data]
121- fmt.Printf("Form values: %v\n", sanitizedParams)122+ // With those values we can use gotemplate/txt to create a custom docker run command
123+ cmdParams, err := render.FromInputsToCmdParams(sanitizedInputs) // Make sure cmdParams is of type render.ContainerCommandParams
124+ if err != nil {
125+ return sendError(c, err)
126+ }
127+
128+ // Create the container
129+ err = docker.CreateContainer(cmdParams)
130+ if err != nil {
131+ return sendError(c, err)
132+ }
122 133
123- return c.String(200, "OK")134+ return c.String(200, "Container created")
124 }135 }
M internal/httpserve/router.go +20 -5
@@ -67,30 +67,45 @@ func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) {
6767 }
6868
6969 func bindHTMXEndpoints(e *echo.Echo, a *app.App) {
70+ // List all images component
7071 e.GET("/htmx/image-manager", func(c echo.Context) error {
71- return handler.ImageManagerHandler(c, a)
72+ return handler.ImageManagerComponent(c, a)
7273 })
74+ // Delete an image
7375 e.DELETE("/htmx/image-manager/delete/:ID", func(c echo.Context) error {
74- return handler.ImageManagerDeleteHandler(c, a)
76+ return handler.ImageManagerDelete(c, a)
7577 })
78+
79+ // List all containers
7680 e.GET("/htmx/container-manager", func(c echo.Context) error {
77- return handler.ContainerManagerHandler(c, a)
81+ return handler.ContainerManagerComponent(c, a)
7882 })
83+ // Stop a container
7984 e.POST("/htmx/container-manager/stop/:ID", func(c echo.Context) error {
80- return handler.ContainerManagerStopHandler(c, a)
85+ return handler.ContainerManagerStop(c, a)
8186 })
87+ // Delete a container
8288 e.DELETE("/htmx/container-manager/delete/:ID", func(c echo.Context) error {
83- return handler.ContainerManagerDeleteHandler(c, a)
89+ return handler.ContainerManagerDelete(c, a)
90+ })
91+ // Start a container
92+ e.POST("/htmx/container-manager/start/:ID", func(c echo.Context) error {
93+ return handler.ContainerManagerStart(c, a)
8494 })
95+
96+ // Display upload-image component
8597 e.GET("/htmx/upload-image", func(c echo.Context) error {
8698 return handler.UploadImageGETHandler(c, a)
8799 })
100+ // Upload image
88101 e.POST("/htmx/upload-image", func(c echo.Context) error {
89102 return handler.UploadImagePOSTHandler(c, a)
90103 })
104+ // Display create-container component
91105 e.GET("htmx/create-container/:ID", func(c echo.Context) error {
92106 return handler.CreateContainerGET(c, a)
93107 })
108+ // Create container
94109 e.POST("htmx/create-container/:ID", func(c echo.Context) error {
95110 return handler.CreateContainerPOST(c, a)
96111 })
@@ -67,30 +67,45 @@ func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) {
67 }67 }
68 68
69 func bindHTMXEndpoints(e *echo.Echo, a *app.App) {69 func bindHTMXEndpoints(e *echo.Echo, a *app.App) {
70+ // List all images component
70 e.GET("/htmx/image-manager", func(c echo.Context) error {71 e.GET("/htmx/image-manager", func(c echo.Context) error {
71- return handler.ImageManagerHandler(c, a)72+ return handler.ImageManagerComponent(c, a)
72 })73 })
74+ // Delete an image
73 e.DELETE("/htmx/image-manager/delete/:ID", func(c echo.Context) error {75 e.DELETE("/htmx/image-manager/delete/:ID", func(c echo.Context) error {
74- return handler.ImageManagerDeleteHandler(c, a)76+ return handler.ImageManagerDelete(c, a)
75 })77 })
78+
79+ // List all containers
76 e.GET("/htmx/container-manager", func(c echo.Context) error {80 e.GET("/htmx/container-manager", func(c echo.Context) error {
77- return handler.ContainerManagerHandler(c, a)81+ return handler.ContainerManagerComponent(c, a)
78 })82 })
83+ // Stop a container
79 e.POST("/htmx/container-manager/stop/:ID", func(c echo.Context) error {84 e.POST("/htmx/container-manager/stop/:ID", func(c echo.Context) error {
80- return handler.ContainerManagerStopHandler(c, a)85+ return handler.ContainerManagerStop(c, a)
81 })86 })
87+ // Delete a container
82 e.DELETE("/htmx/container-manager/delete/:ID", func(c echo.Context) error {88 e.DELETE("/htmx/container-manager/delete/:ID", func(c echo.Context) error {
83- return handler.ContainerManagerDeleteHandler(c, a)89+ return handler.ContainerManagerDelete(c, a)
90+ })
91+ // Start a container
92+ e.POST("/htmx/container-manager/start/:ID", func(c echo.Context) error {
93+ return handler.ContainerManagerStart(c, a)
84 })94 })
95+
96+ // Display upload-image component
85 e.GET("/htmx/upload-image", func(c echo.Context) error {97 e.GET("/htmx/upload-image", func(c echo.Context) error {
86 return handler.UploadImageGETHandler(c, a)98 return handler.UploadImageGETHandler(c, a)
87 })99 })
100+ // Upload image
88 e.POST("/htmx/upload-image", func(c echo.Context) error {101 e.POST("/htmx/upload-image", func(c echo.Context) error {
89 return handler.UploadImagePOSTHandler(c, a)102 return handler.UploadImagePOSTHandler(c, a)
90 })103 })
104+ // Display create-container component
91 e.GET("htmx/create-container/:ID", func(c echo.Context) error {105 e.GET("htmx/create-container/:ID", func(c echo.Context) error {
92 return handler.CreateContainerGET(c, a)106 return handler.CreateContainerGET(c, a)
93 })107 })
108+ // Create container
94 e.POST("htmx/create-container/:ID", func(c echo.Context) error {109 e.POST("htmx/create-container/:ID", func(c echo.Context) error {
95 return handler.CreateContainerPOST(c, a)110 return handler.CreateContainerPOST(c, a)
96 })111 })
M internal/webui/public/assets/css/tailwind.css +23 -58
@@ -580,11 +580,6 @@ video {
580580 margin-bottom: 0.5rem;
581581 }
582582
583-.mx-2 {
584- margin-left: 0.5rem;
585- margin-right: 0.5rem;
586-}
587-
588583 .mb-1 {
589584 margin-bottom: 0.25rem;
590585 }
@@ -665,15 +660,23 @@ video {
665660 width: 100%;
666661 }
667662
663+.w-1\/3 {
664+ width: 33.333333%;
665+}
666+
667+.w-1\/4 {
668+ width: 25%;
669+}
670+
671+.w-5\/12 {
672+ width: 41.666667%;
673+}
674+
668675 .min-w-fit {
669676 min-width: -moz-fit-content;
670677 min-width: fit-content;
671678 }
672679
673-.max-w-lg {
674- max-width: 32rem;
675-}
676-
677680 .max-w-fit {
678681 max-width: -moz-fit-content;
679682 max-width: fit-content;
@@ -715,22 +718,16 @@ video {
715718 justify-content: space-between;
716719 }
717720
718-.justify-around {
719- justify-content: space-around;
720-}
721-
722-.justify-evenly {
723- justify-content: space-evenly;
724-}
725-
726-.justify-stretch {
727- justify-content: stretch;
728-}
729-
730721 .justify-items-center {
731722 justify-items: center;
732723 }
733724
725+.space-x-4 > :not([hidden]) ~ :not([hidden]) {
726+ --tw-space-x-reverse: 0;
727+ margin-right: calc(1rem * var(--tw-space-x-reverse));
728+ margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
729+}
730+
734731 .space-y-2 > :not([hidden]) ~ :not([hidden]) {
735732 --tw-space-y-reverse: 0;
736733 margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
@@ -743,12 +740,6 @@ video {
743740 margin-bottom: calc(1rem * var(--tw-space-y-reverse));
744741 }
745742
746-.space-x-4 > :not([hidden]) ~ :not([hidden]) {
747- --tw-space-x-reverse: 0;
748- margin-right: calc(1rem * var(--tw-space-x-reverse));
749- margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
750-}
751-
752743 .rounded {
753744 border-radius: 0.25rem;
754745 }
@@ -843,10 +834,6 @@ video {
843834 padding: 1rem;
844835 }
845836
846-.p-8 {
847- padding: 2rem;
848-}
849-
850837 .px-2 {
851838 padding-left: 0.5rem;
852839 padding-right: 0.5rem;
@@ -871,14 +858,6 @@ video {
871858 padding-bottom: 1rem;
872859 }
873860
874-.pl-2 {
875- padding-left: 0.5rem;
876-}
877-
878-.text-center {
879- text-align: center;
880-}
881-
882861 .align-middle {
883862 vertical-align: middle;
884863 }
@@ -950,6 +929,11 @@ video {
950929 color: rgb(107 114 128 / var(--tw-text-opacity));
951930 }
952931
932+.text-gray-600 {
933+ --tw-text-opacity: 1;
934+ color: rgb(75 85 99 / var(--tw-text-opacity));
935+}
936+
953937 .text-gray-800 {
954938 --tw-text-opacity: 1;
955939 color: rgb(31 41 55 / var(--tw-text-opacity));
@@ -975,21 +959,6 @@ video {
975959 color: rgb(255 255 255 / var(--tw-text-opacity));
976960 }
977961
978-.text-gray-300 {
979- --tw-text-opacity: 1;
980- color: rgb(209 213 219 / var(--tw-text-opacity));
981-}
982-
983-.text-gray-400 {
984- --tw-text-opacity: 1;
985- color: rgb(156 163 175 / var(--tw-text-opacity));
986-}
987-
988-.text-gray-600 {
989- --tw-text-opacity: 1;
990- color: rgb(75 85 99 / var(--tw-text-opacity));
991-}
992-
993962 .underline {
994963 text-decoration-line: underline;
995964 }
@@ -1204,10 +1173,6 @@ video {
12041173 padding-bottom: 2rem;
12051174 }
12061175
1207- .md\:pl-4 {
1208- padding-left: 1rem;
1209- }
1210-
12111176 .md\:pt-10 {
12121177 padding-top: 2.5rem;
12131178 }
@@ -580,11 +580,6 @@ video {
580 margin-bottom: 0.5rem;580 margin-bottom: 0.5rem;
581 }581 }
582 582
583-.mx-2 {
584- margin-left: 0.5rem;
585- margin-right: 0.5rem;
586-}
587-
588 .mb-1 {583 .mb-1 {
589 margin-bottom: 0.25rem;584 margin-bottom: 0.25rem;
590 }585 }
@@ -665,15 +660,23 @@ video {
665 width: 100%;660 width: 100%;
666 }661 }
667 662
663+.w-1\/3 {
664+ width: 33.333333%;
665+}
666+
667+.w-1\/4 {
668+ width: 25%;
669+}
670+
671+.w-5\/12 {
672+ width: 41.666667%;
673+}
674+
668 .min-w-fit {675 .min-w-fit {
669 min-width: -moz-fit-content;676 min-width: -moz-fit-content;
670 min-width: fit-content;677 min-width: fit-content;
671 }678 }
672 679
673-.max-w-lg {
674- max-width: 32rem;
675-}
676-
677 .max-w-fit {680 .max-w-fit {
678 max-width: -moz-fit-content;681 max-width: -moz-fit-content;
679 max-width: fit-content;682 max-width: fit-content;
@@ -715,22 +718,16 @@ video {
715 justify-content: space-between;718 justify-content: space-between;
716 }719 }
717 720
718-.justify-around {
719- justify-content: space-around;
720-}
721-
722-.justify-evenly {
723- justify-content: space-evenly;
724-}
725-
726-.justify-stretch {
727- justify-content: stretch;
728-}
729-
730 .justify-items-center {721 .justify-items-center {
731 justify-items: center;722 justify-items: center;
732 }723 }
733 724
725+.space-x-4 > :not([hidden]) ~ :not([hidden]) {
726+ --tw-space-x-reverse: 0;
727+ margin-right: calc(1rem * var(--tw-space-x-reverse));
728+ margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
729+}
730+
734 .space-y-2 > :not([hidden]) ~ :not([hidden]) {731 .space-y-2 > :not([hidden]) ~ :not([hidden]) {
735 --tw-space-y-reverse: 0;732 --tw-space-y-reverse: 0;
736 margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));733 margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
@@ -743,12 +740,6 @@ video {
743 margin-bottom: calc(1rem * var(--tw-space-y-reverse));740 margin-bottom: calc(1rem * var(--tw-space-y-reverse));
744 }741 }
745 742
746-.space-x-4 > :not([hidden]) ~ :not([hidden]) {
747- --tw-space-x-reverse: 0;
748- margin-right: calc(1rem * var(--tw-space-x-reverse));
749- margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
750-}
751-
752 .rounded {743 .rounded {
753 border-radius: 0.25rem;744 border-radius: 0.25rem;
754 }745 }
@@ -843,10 +834,6 @@ video {
843 padding: 1rem;834 padding: 1rem;
844 }835 }
845 836
846-.p-8 {
847- padding: 2rem;
848-}
849-
850 .px-2 {837 .px-2 {
851 padding-left: 0.5rem;838 padding-left: 0.5rem;
852 padding-right: 0.5rem;839 padding-right: 0.5rem;
@@ -871,14 +858,6 @@ video {
871 padding-bottom: 1rem;858 padding-bottom: 1rem;
872 }859 }
873 860
874-.pl-2 {
875- padding-left: 0.5rem;
876-}
877-
878-.text-center {
879- text-align: center;
880-}
881-
882 .align-middle {861 .align-middle {
883 vertical-align: middle;862 vertical-align: middle;
884 }863 }
@@ -950,6 +929,11 @@ video {
950 color: rgb(107 114 128 / var(--tw-text-opacity));929 color: rgb(107 114 128 / var(--tw-text-opacity));
951 }930 }
952 931
932+.text-gray-600 {
933+ --tw-text-opacity: 1;
934+ color: rgb(75 85 99 / var(--tw-text-opacity));
935+}
936+
953 .text-gray-800 {937 .text-gray-800 {
954 --tw-text-opacity: 1;938 --tw-text-opacity: 1;
955 color: rgb(31 41 55 / var(--tw-text-opacity));939 color: rgb(31 41 55 / var(--tw-text-opacity));
@@ -975,21 +959,6 @@ video {
975 color: rgb(255 255 255 / var(--tw-text-opacity));959 color: rgb(255 255 255 / var(--tw-text-opacity));
976 }960 }
977 961
978-.text-gray-300 {
979- --tw-text-opacity: 1;
980- color: rgb(209 213 219 / var(--tw-text-opacity));
981-}
982-
983-.text-gray-400 {
984- --tw-text-opacity: 1;
985- color: rgb(156 163 175 / var(--tw-text-opacity));
986-}
987-
988-.text-gray-600 {
989- --tw-text-opacity: 1;
990- color: rgb(75 85 99 / var(--tw-text-opacity));
991-}
992-
993 .underline {962 .underline {
994 text-decoration-line: underline;963 text-decoration-line: underline;
995 }964 }
@@ -1204,10 +1173,6 @@ video {
1204 padding-bottom: 2rem;1173 padding-bottom: 2rem;
1205 }1174 }
1206 1175
1207- .md\:pl-4 {
1208- padding-left: 1rem;
1209- }
1210-
1211 .md\:pt-10 {1176 .md\:pt-10 {
1212 padding-top: 2.5rem;1177 padding-top: 2.5rem;
1213 }1178 }
M pkg/utils/docker/containers.go +112 -4
@@ -1,15 +1,33 @@
11 package docker
22
33 import (
4+ "bytes"
45 "context"
6+ "strings"
57 "time"
68
79 "github.com/docker/docker/api/types"
10+ "github.com/docker/docker/api/types/container"
11+ "github.com/docker/docker/client"
12+ "github.com/docker/go-connections/nat"
813 )
914
10-func init() {
15+type ContainerCommandParams struct {
16+ ContainerName string
17+ ContainerHost string
18+ Domain string
19+ Https bool
20+ EnvVar string
21+ ImageName string
22+ ImageID string
23+ Ports string
24+ Data string
25+ TraefikLabels []string
26+ Network string
27+ Restart string
1128 }
1229
30+// ListRunningContainers lists all running containers
1331 func ListRunningContainers() ([]types.Container, error) {
1432 // Check if the Docker client has been initialized
1533 CheckIfInitialized()
@@ -18,12 +36,10 @@ func ListRunningContainers() ([]types.Container, error) {
1836 if err != nil {
1937 return nil, err
2038 }
21-
2239 return containers, nil
2340 }
2441
25-// StopContainer attempts to stop a container gracefully with a timeout
26-// If it doesn't stop, it sets a flag that can be checked to prompt the user.
42+// StopContainerGracefully stops a container by sending a SIGTERM and waiting for it to stop
2743 func StopContainerGracefully(containerID string, timeoutDuration time.Duration) (bool, error) {
2844
2945 // Start by sending a SIGTERM
@@ -56,6 +72,7 @@ func StopContainerGracefully(containerID string, timeoutDuration time.Duration)
5672 return false, nil
5773 }
5874
75+// StopContainerRagefully stops a container by sending a SIGKILL
5976 func StopContainerRagefully(containerID string) error {
6077 // Start by sending a SIGKILL
6178 err := dockerCli.ContainerKill(context.Background(), containerID, "SIGKILL")
@@ -66,6 +83,7 @@ func StopContainerRagefully(containerID string) error {
6683 return nil
6784 }
6885
86+// DeleteContainer deletes a container
6987 func DeleteContainer(containerID string) error {
7088
7189 // Delete container using the Docker client
@@ -76,3 +94,93 @@ func DeleteContainer(containerID string) error {
7694
7795 return nil
7896 }
97+
98+// StartContainer starts a container
99+func StartContainer(containerID string) error {
100+
101+ // Start container using the Docker client
102+ err := dockerCli.ContainerStart(context.Background(), containerID, types.ContainerStartOptions{})
103+ if err != nil {
104+ return err
105+ }
106+
107+ return nil
108+}
109+
110+// CreateContainer creates a container with the given parameters
111+func CreateContainer(cmdParams ContainerCommandParams) error {
112+ // Initialize Docker client
113+ dockerCli, err := client.NewClientWithOpts(client.FromEnv)
114+ if err != nil {
115+ return err
116+ }
117+
118+ // Prepare container port bindings
119+ portBindings := nat.PortMap{}
120+ hostConfig := &container.HostConfig{
121+ PortBindings: portBindings,
122+ RestartPolicy: container.RestartPolicy{
123+ Name: cmdParams.Restart,
124+ },
125+ }
126+
127+ // Define port mapping
128+ parts := strings.Split(cmdParams.Ports, ":")
129+ if len(parts) == 2 {
130+ portBindings[nat.Port(parts[1])] = []nat.PortBinding{
131+ {HostIP: "0.0.0.0", HostPort: parts[0]},
132+ }
133+ }
134+
135+ // Create container
136+ resp, err := dockerCli.ContainerCreate(
137+ context.Background(),
138+ &container.Config{
139+ Image: cmdParams.ImageName,
140+ Labels: map[string]string{},
141+ },
142+ hostConfig,
143+ nil, nil,
144+ cmdParams.ContainerName,
145+ )
146+ if err != nil {
147+ return err
148+ }
149+
150+ // Start container
151+ err = dockerCli.ContainerStart(context.Background(), resp.ID, types.ContainerStartOptions{})
152+ if err != nil {
153+ return err
154+ }
155+
156+ return nil
157+}
158+
159+// GetContainerInfo returns information about a container
160+func GetContainerInfo(containerID string) (types.ContainerJSON, error) {
161+
162+ // Get container info using the Docker client
163+ containerInfo, err := dockerCli.ContainerInspect(context.Background(), containerID)
164+ if err != nil {
165+ return types.ContainerJSON{}, err
166+ }
167+
168+ return containerInfo, nil
169+}
170+
171+// GetContainerLogs returns the logs of a container
172+func GetContainerLogs(containerID string) (string, error) {
173+
174+ // Get container logs using the Docker client
175+ containerLogs, err := dockerCli.ContainerLogs(context.Background(), containerID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true})
176+ if err != nil {
177+ return "", err
178+ }
179+
180+ // Read the logs
181+ buf := new(bytes.Buffer)
182+ buf.ReadFrom(containerLogs)
183+ containerLogsString := buf.String()
184+
185+ return containerLogsString, nil
186+}
@@ -1,15 +1,33 @@
1 package docker1 package docker
2 2
3 import (3 import (
4+ "bytes"
4 "context"5 "context"
6+ "strings"
5 "time"7 "time"
6 8
7 "github.com/docker/docker/api/types"9 "github.com/docker/docker/api/types"
10+ "github.com/docker/docker/api/types/container"
11+ "github.com/docker/docker/client"
12+ "github.com/docker/go-connections/nat"
8 )13 )
9 14
10-func init() {15+type ContainerCommandParams struct {
16+ ContainerName string
17+ ContainerHost string
18+ Domain string
19+ Https bool
20+ EnvVar string
21+ ImageName string
22+ ImageID string
23+ Ports string
24+ Data string
25+ TraefikLabels []string
26+ Network string
27+ Restart string
11 }28 }
12 29
30+// ListRunningContainers lists all running containers
13 func ListRunningContainers() ([]types.Container, error) {31 func ListRunningContainers() ([]types.Container, error) {
14 // Check if the Docker client has been initialized32 // Check if the Docker client has been initialized
15 CheckIfInitialized()33 CheckIfInitialized()
@@ -18,12 +36,10 @@ func ListRunningContainers() ([]types.Container, error) {
18 if err != nil {36 if err != nil {
19 return nil, err37 return nil, err
20 }38 }
21-
22 return containers, nil39 return containers, nil
23 }40 }
24 41
25-// StopContainer attempts to stop a container gracefully with a timeout42+// StopContainerGracefully stops a container by sending a SIGTERM and waiting for it to stop
26-// If it doesn't stop, it sets a flag that can be checked to prompt the user.
27 func StopContainerGracefully(containerID string, timeoutDuration time.Duration) (bool, error) {43 func StopContainerGracefully(containerID string, timeoutDuration time.Duration) (bool, error) {
28 44
29 // Start by sending a SIGTERM45 // Start by sending a SIGTERM
@@ -56,6 +72,7 @@ func StopContainerGracefully(containerID string, timeoutDuration time.Duration)
56 return false, nil72 return false, nil
57 }73 }
58 74
75+// StopContainerRagefully stops a container by sending a SIGKILL
59 func StopContainerRagefully(containerID string) error {76 func StopContainerRagefully(containerID string) error {
60 // Start by sending a SIGKILL77 // Start by sending a SIGKILL
61 err := dockerCli.ContainerKill(context.Background(), containerID, "SIGKILL")78 err := dockerCli.ContainerKill(context.Background(), containerID, "SIGKILL")
@@ -66,6 +83,7 @@ func StopContainerRagefully(containerID string) error {
66 return nil83 return nil
67 }84 }
68 85
86+// DeleteContainer deletes a container
69 func DeleteContainer(containerID string) error {87 func DeleteContainer(containerID string) error {
70 88
71 // Delete container using the Docker client89 // Delete container using the Docker client
@@ -76,3 +94,93 @@ func DeleteContainer(containerID string) error {
76 94
77 return nil95 return nil
78 }96 }
97+
98+// StartContainer starts a container
99+func StartContainer(containerID string) error {
100+
101+ // Start container using the Docker client
102+ err := dockerCli.ContainerStart(context.Background(), containerID, types.ContainerStartOptions{})
103+ if err != nil {
104+ return err
105+ }
106+
107+ return nil
108+}
109+
110+// CreateContainer creates a container with the given parameters
111+func CreateContainer(cmdParams ContainerCommandParams) error {
112+ // Initialize Docker client
113+ dockerCli, err := client.NewClientWithOpts(client.FromEnv)
114+ if err != nil {
115+ return err
116+ }
117+
118+ // Prepare container port bindings
119+ portBindings := nat.PortMap{}
120+ hostConfig := &container.HostConfig{
121+ PortBindings: portBindings,
122+ RestartPolicy: container.RestartPolicy{
123+ Name: cmdParams.Restart,
124+ },
125+ }
126+
127+ // Define port mapping
128+ parts := strings.Split(cmdParams.Ports, ":")
129+ if len(parts) == 2 {
130+ portBindings[nat.Port(parts[1])] = []nat.PortBinding{
131+ {HostIP: "0.0.0.0", HostPort: parts[0]},
132+ }
133+ }
134+
135+ // Create container
136+ resp, err := dockerCli.ContainerCreate(
137+ context.Background(),
138+ &container.Config{
139+ Image: cmdParams.ImageName,
140+ Labels: map[string]string{},
141+ },
142+ hostConfig,
143+ nil, nil,
144+ cmdParams.ContainerName,
145+ )
146+ if err != nil {
147+ return err
148+ }
149+
150+ // Start container
151+ err = dockerCli.ContainerStart(context.Background(), resp.ID, types.ContainerStartOptions{})
152+ if err != nil {
153+ return err
154+ }
155+
156+ return nil
157+}
158+
159+// GetContainerInfo returns information about a container
160+func GetContainerInfo(containerID string) (types.ContainerJSON, error) {
161+
162+ // Get container info using the Docker client
163+ containerInfo, err := dockerCli.ContainerInspect(context.Background(), containerID)
164+ if err != nil {
165+ return types.ContainerJSON{}, err
166+ }
167+
168+ return containerInfo, nil
169+}
170+
171+// GetContainerLogs returns the logs of a container
172+func GetContainerLogs(containerID string) (string, error) {
173+
174+ // Get container logs using the Docker client
175+ containerLogs, err := dockerCli.ContainerLogs(context.Background(), containerID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true})
176+ if err != nil {
177+ return "", err
178+ }
179+
180+ // Read the logs
181+ buf := new(bytes.Buffer)
182+ buf.ReadFrom(containerLogs)
183+ containerLogsString := buf.String()
184+
185+ return containerLogsString, nil
186+}

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