Enhanced create container form. Some Logic refactoring on the router.
- commit
- 0243b0c2ff2d19a808dc2ec778f72801e0de608c
- author
- bnema · · 2023-09-26 17:35 UTC
- parent
- ed6c5eb
- signature
- unsigned
8 files · +326 -125
M internal/gotemplate/models/html/fragments/createcontainer.gohtml +86 -23
| @@ -1,5 +1,5 @@ | |||
| 1 | 1 | <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 | 4 | <!-- Left Side: Docker Run Options --> | |
| 5 | 5 | <div class="flex-grow bg-slate-100 p-4 rounded-md"> | |
| @@ -12,20 +12,35 @@ | |||
| 12 | 12 | method="post" | |
| 13 | 13 | hx-post="/htmx/create-container/{{.ShortID}}" | |
| 14 | 14 | hx-swap="innerHTML" | |
| 15 | + | hx-target-*="#error-message-create-container-{{.ShortID}}" | |
| 15 | 16 | > | |
| 16 | 17 | <!-- 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> | |
| 26 | 40 | </div> | |
| 41 | + | ||
| 27 | 42 | <!-- Container Name --> | |
| 28 | - | <div class="mb-4"> | |
| 43 | + | <div class="pb-4"> | |
| 29 | 44 | <label class="block">Container name</label> | |
| 30 | 45 | <input | |
| 31 | 46 | type="text" | |
| @@ -34,19 +49,41 @@ | |||
| 34 | 49 | value="{{.ContainerName}}" | |
| 35 | 50 | /> | |
| 36 | 51 | </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> | |
| 46 | 84 | </div> | |
| 47 | - | ||
| 48 | 85 | <!-- Ports --> | |
| 49 | - | <div class="mb-4"> | |
| 86 | + | <div class="pb-4"> | |
| 50 | 87 | <label class="block">Ports</label> | |
| 51 | 88 | <span class="text-gray-500" | |
| 52 | 89 | >e.g. 80:80 (left is host, right is container)</span | |
| @@ -60,7 +97,7 @@ | |||
| 60 | 97 | </div> | |
| 61 | 98 | ||
| 62 | 99 | <!-- Volumes --> | |
| 63 | - | <div class="mb-4"> | |
| 100 | + | <div class="pb-4"> | |
| 64 | 101 | <label class="block">Volumes</label> | |
| 65 | 102 | <span class="text-gray-500">e.g. ./data:/container/data </span> | |
| 66 | 103 | <input | |
| @@ -71,11 +108,37 @@ | |||
| 71 | 108 | /> | |
| 72 | 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 | 136 | <input | |
| 75 | 137 | type="submit" | |
| 76 | 138 | value="Create" | |
| 77 | 139 | class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" | |
| 78 | 140 | /> | |
| 79 | 141 | </form> | |
| 142 | + | <div id="error-message-create-container-{{.ShortID}}" class="text-red-500"></div> | |
| 80 | 143 | </div> | |
| 81 | 144 | </div> | |
| @@ -1,5 +1,5 @@ | |||||
| 1 | <div | 1 | <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 | - | <input | 20 | + | <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 | - | readonly | 25 | + | 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 | <input | 45 | <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 | - | <input | 55 | + | <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)</span | 89 | >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 | <input | 103 | <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 | <input | 136 | <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 { | |||
| 36 | 36 | StateColor string | |
| 37 | 37 | } | |
| 38 | 38 | ||
| 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 { | |
| 41 | 41 | images, err := docker.ListContainerImages() | |
| 42 | 42 | if err != nil { | |
| 43 | 43 | return sendError(c, err) | |
| @@ -78,8 +78,8 @@ func ImageManagerHandler(c echo.Context, a *app.App) error { | |||
| 78 | 78 | return c.HTML(200, renderedHTML) | |
| 79 | 79 | } | |
| 80 | 80 | ||
| 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 { | |
| 83 | 83 | // Get the ShortImgID from the URL | |
| 84 | 84 | ShortID := c.Param("ID") | |
| 85 | 85 | ||
| @@ -107,8 +107,8 @@ func ImageManagerDeleteHandler(c echo.Context, a *app.App) error { | |||
| 107 | 107 | return c.HTML(http.StatusOK, `<div>Removed</div>`) | |
| 108 | 108 | } | |
| 109 | 109 | ||
| 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 { | |
| 112 | 112 | containers, err := docker.ListRunningContainers() | |
| 113 | 113 | if err != nil { | |
| 114 | 114 | return sendError(c, err) | |
| @@ -117,8 +117,7 @@ func ContainerManagerHandler(c echo.Context, a *app.App) error { | |||
| 117 | 117 | var humanReadableContainers []HumanReadableContainer | |
| 118 | 118 | ||
| 119 | 119 | for _, container := range containers { | |
| 120 | - | ShortID := container.ID[0:12] | |
| 121 | - | idMap[ShortID] = container.ID | |
| 120 | + | localContainer := container // Make a local copy | |
| 122 | 121 | createdTime := time.Unix(container.Created, 0) | |
| 123 | 122 | createdStr := humanize.TimeAgo(createdTime) | |
| 124 | 123 | sizeStr := humanize.BytesToReadableSize(container.SizeRw) | |
| @@ -136,13 +135,12 @@ func ContainerManagerHandler(c echo.Context, a *app.App) error { | |||
| 136 | 135 | for _, name := range container.Names { | |
| 137 | 136 | name = name[1:] | |
| 138 | 137 | humanReadableContainers = append(humanReadableContainers, HumanReadableContainer{ | |
| 139 | - | Container: &container, | |
| 138 | + | Container: &localContainer, | |
| 140 | 139 | CreatedStr: createdStr, | |
| 141 | 140 | SizeStr: sizeStr, | |
| 142 | 141 | UpSince: humanize.TimeAgo(time.Unix(container.Created, 0)), | |
| 143 | 142 | StateColor: stateColor, | |
| 144 | 143 | Name: name, | |
| 145 | - | ShortID: ShortID, | |
| 146 | 144 | Ports: ports, | |
| 147 | 145 | }) | |
| 148 | 146 | } | |
| @@ -162,8 +160,8 @@ func ContainerManagerHandler(c echo.Context, a *app.App) error { | |||
| 162 | 160 | return c.HTML(200, renderedHTML) | |
| 163 | 161 | } | |
| 164 | 162 | ||
| 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 { | |
| 167 | 165 | ID := c.Param("ID") | |
| 168 | 166 | err := docker.DeleteContainer(ID) | |
| 169 | 167 | if err != nil { | |
| @@ -172,8 +170,8 @@ func ContainerManagerDeleteHandler(c echo.Context, a *app.App) error { | |||
| 172 | 170 | return c.HTML(http.StatusOK, `Success`) | |
| 173 | 171 | } | |
| 174 | 172 | ||
| 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 { | |
| 177 | 175 | ID := c.Param("ID") | |
| 178 | 176 | // Stop the container gracefully with a timeout | |
| 179 | 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 | 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 | + | } | |
| @@ -36,8 +36,8 @@ type HumanReadableContainer struct { | |||||
| 36 | StateColor string | 36 | 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 route | 81 | + | // 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 URL | 83 | // 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 route | 110 | + | // 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 []HumanReadableContainer | 117 | 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 route | 163 | + | // 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 route | 173 | + | // 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 timeout | 176 | // 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 { | |||
| 62 | 62 | data := map[string]interface{}{ | |
| 63 | 63 | "Title": "Create a new container", | |
| 64 | 64 | "ShortID": ShortID, | |
| 65 | + | "ImageID": imageID, | |
| 65 | 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 | 107 | // Initialize a map to store sanitized values | |
| 107 | - | sanitizedParams := make(map[string]string) | |
| 108 | + | sanitizedInputs := make(map[string]string) | |
| 108 | 109 | ||
| 109 | 110 | // Iterate over all form parameters and sanitize them | |
| 110 | 111 | for k, v := range formParams { | |
| @@ -113,12 +114,22 @@ func CreateContainerPOST(c echo.Context, a *app.App) error { | |||
| 113 | 114 | if err != nil { | |
| 114 | 115 | return sendError(c, err) | |
| 115 | 116 | } | |
| 116 | - | sanitizedParams[k] = sanitized | |
| 117 | + | sanitizedInputs[k] = sanitized | |
| 117 | 118 | } | |
| 118 | 119 | } | |
| 119 | 120 | ||
| 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 | + | } | |
| 122 | 133 | ||
| 123 | - | return c.String(200, "OK") | |
| 134 | + | return c.String(200, "Container created") | |
| 124 | 135 | } | |
| @@ -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 values | 107 | // 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 them | 110 | // 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] = sanitized | 117 | + | sanitizedInputs[k] = sanitized |
| 117 | } | 118 | } | ||
| 118 | } | 119 | } | ||
| 119 | 120 | ||||
| 120 | - | // Print the form values | 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] |
| 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) { | |||
| 67 | 67 | } | |
| 68 | 68 | ||
| 69 | 69 | func bindHTMXEndpoints(e *echo.Echo, a *app.App) { | |
| 70 | + | // List all images component | |
| 70 | 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 | 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 | 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 | 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 | 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 | 97 | e.GET("/htmx/upload-image", func(c echo.Context) error { | |
| 86 | 98 | return handler.UploadImageGETHandler(c, a) | |
| 87 | 99 | }) | |
| 100 | + | // Upload image | |
| 88 | 101 | e.POST("/htmx/upload-image", func(c echo.Context) error { | |
| 89 | 102 | return handler.UploadImagePOSTHandler(c, a) | |
| 90 | 103 | }) | |
| 104 | + | // Display create-container component | |
| 91 | 105 | e.GET("htmx/create-container/:ID", func(c echo.Context) error { | |
| 92 | 106 | return handler.CreateContainerGET(c, a) | |
| 93 | 107 | }) | |
| 108 | + | // Create container | |
| 94 | 109 | e.POST("htmx/create-container/:ID", func(c echo.Context) error { | |
| 95 | 110 | return handler.CreateContainerPOST(c, a) | |
| 96 | 111 | }) | |
| @@ -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 { | |||
| 580 | 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 | 583 | .mb-1 { | |
| 589 | 584 | margin-bottom: 0.25rem; | |
| 590 | 585 | } | |
| @@ -665,15 +660,23 @@ video { | |||
| 665 | 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 | 675 | .min-w-fit { | |
| 669 | 676 | min-width: -moz-fit-content; | |
| 670 | 677 | min-width: fit-content; | |
| 671 | 678 | } | |
| 672 | 679 | ||
| 673 | - | .max-w-lg { | |
| 674 | - | max-width: 32rem; | |
| 675 | - | } | |
| 676 | - | ||
| 677 | 680 | .max-w-fit { | |
| 678 | 681 | max-width: -moz-fit-content; | |
| 679 | 682 | max-width: fit-content; | |
| @@ -715,22 +718,16 @@ video { | |||
| 715 | 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 | 721 | .justify-items-center { | |
| 731 | 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 | 731 | .space-y-2 > :not([hidden]) ~ :not([hidden]) { | |
| 735 | 732 | --tw-space-y-reverse: 0; | |
| 736 | 733 | margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); | |
| @@ -743,12 +740,6 @@ video { | |||
| 743 | 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 | 743 | .rounded { | |
| 753 | 744 | border-radius: 0.25rem; | |
| 754 | 745 | } | |
| @@ -843,10 +834,6 @@ video { | |||
| 843 | 834 | padding: 1rem; | |
| 844 | 835 | } | |
| 845 | 836 | ||
| 846 | - | .p-8 { | |
| 847 | - | padding: 2rem; | |
| 848 | - | } | |
| 849 | - | ||
| 850 | 837 | .px-2 { | |
| 851 | 838 | padding-left: 0.5rem; | |
| 852 | 839 | padding-right: 0.5rem; | |
| @@ -871,14 +858,6 @@ video { | |||
| 871 | 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 | 861 | .align-middle { | |
| 883 | 862 | vertical-align: middle; | |
| 884 | 863 | } | |
| @@ -950,6 +929,11 @@ video { | |||
| 950 | 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 | 937 | .text-gray-800 { | |
| 954 | 938 | --tw-text-opacity: 1; | |
| 955 | 939 | color: rgb(31 41 55 / var(--tw-text-opacity)); | |
| @@ -975,21 +959,6 @@ video { | |||
| 975 | 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 | 962 | .underline { | |
| 994 | 963 | text-decoration-line: underline; | |
| 995 | 964 | } | |
| @@ -1204,10 +1173,6 @@ video { | |||
| 1204 | 1173 | padding-bottom: 2rem; | |
| 1205 | 1174 | } | |
| 1206 | 1175 | ||
| 1207 | - | .md\:pl-4 { | |
| 1208 | - | padding-left: 1rem; | |
| 1209 | - | } | |
| 1210 | - | ||
| 1211 | 1176 | .md\:pt-10 { | |
| 1212 | 1177 | padding-top: 2.5rem; | |
| 1213 | 1178 | } | |
| @@ -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 @@ | |||
| 1 | 1 | package docker | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "bytes" | |
| 4 | 5 | "context" | |
| 6 | + | "strings" | |
| 5 | 7 | "time" | |
| 6 | 8 | ||
| 7 | 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 | 31 | func ListRunningContainers() ([]types.Container, error) { | |
| 14 | 32 | // Check if the Docker client has been initialized | |
| 15 | 33 | CheckIfInitialized() | |
| @@ -18,12 +36,10 @@ func ListRunningContainers() ([]types.Container, error) { | |||
| 18 | 36 | if err != nil { | |
| 19 | 37 | return nil, err | |
| 20 | 38 | } | |
| 21 | - | ||
| 22 | 39 | return containers, nil | |
| 23 | 40 | } | |
| 24 | 41 | ||
| 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 | |
| 27 | 43 | func StopContainerGracefully(containerID string, timeoutDuration time.Duration) (bool, error) { | |
| 28 | 44 | ||
| 29 | 45 | // Start by sending a SIGTERM | |
| @@ -56,6 +72,7 @@ func StopContainerGracefully(containerID string, timeoutDuration time.Duration) | |||
| 56 | 72 | return false, nil | |
| 57 | 73 | } | |
| 58 | 74 | ||
| 75 | + | // StopContainerRagefully stops a container by sending a SIGKILL | |
| 59 | 76 | func StopContainerRagefully(containerID string) error { | |
| 60 | 77 | // Start by sending a SIGKILL | |
| 61 | 78 | err := dockerCli.ContainerKill(context.Background(), containerID, "SIGKILL") | |
| @@ -66,6 +83,7 @@ func StopContainerRagefully(containerID string) error { | |||
| 66 | 83 | return nil | |
| 67 | 84 | } | |
| 68 | 85 | ||
| 86 | + | // DeleteContainer deletes a container | |
| 69 | 87 | func DeleteContainer(containerID string) error { | |
| 70 | 88 | ||
| 71 | 89 | // Delete container using the Docker client | |
| @@ -76,3 +94,93 @@ func DeleteContainer(containerID string) error { | |||
| 76 | 94 | ||
| 77 | 95 | 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 | + | } | |
| @@ -1,15 +1,33 @@ | |||||
| 1 | package docker | 1 | 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 initialized | 32 | // 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, err | 37 | return nil, err | ||
| 20 | } | 38 | } | ||
| 21 | - | ||||
| 22 | return containers, nil | 39 | return containers, nil | ||
| 23 | } | 40 | } | ||
| 24 | 41 | ||||
| 25 | - | // StopContainer attempts to stop a container gracefully with a timeout | 42 | + | // 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 SIGTERM | 45 | // Start by sending a SIGTERM | ||
| @@ -56,6 +72,7 @@ func StopContainerGracefully(containerID string, timeoutDuration time.Duration) | |||||
| 56 | return false, nil | 72 | 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 SIGKILL | 77 | // 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 nil | 83 | 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 client | 89 | // Delete container using the Docker client | ||
| @@ -76,3 +94,93 @@ func DeleteContainer(containerID string) error { | |||||
| 76 | 94 | ||||
| 77 | return nil | 95 | 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 | + | } | |||