For each listed images add a deploy button. Pops up a basic deploy form with inputs
- commit
- c410e035cd84e6af35a09978d62bc1007cff56f0
- author
- bnema · · 2023-09-25 05:32 UTC
- committer
- bnema · · 2023-09-25 05:33 UTC
- parent
- 8c01942
- signature
- unsigned
12 files · +248 -32
M internal/app/app.go +1 -0
| @@ -56,6 +56,7 @@ type ContainerEngineConfig struct { | |||
| 56 | 56 | Sock string `yaml:"dockersock"` | |
| 57 | 57 | PodmanSock string `yaml:"podmansock"` | |
| 58 | 58 | Podman bool `yaml:"podman"` | |
| 59 | + | Network string `yaml:"network"` | |
| 59 | 60 | } | |
| 60 | 61 | ||
| 61 | 62 | func InitializeEnvironment() { | |
| @@ -56,6 +56,7 @@ type ContainerEngineConfig struct { | |||||
| 56 | Sock string `yaml:"dockersock"` | 56 | Sock string `yaml:"dockersock"` | ||
| 57 | PodmanSock string `yaml:"podmansock"` | 57 | PodmanSock string `yaml:"podmansock"` | ||
| 58 | Podman bool `yaml:"podman"` | 58 | Podman bool `yaml:"podman"` | ||
| 59 | + | Network string `yaml:"network"` | |||
| 59 | } | 60 | } | ||
| 60 | 61 | ||||
| 61 | func InitializeEnvironment() { | 62 | func InitializeEnvironment() { | ||
M internal/gotemplate/models/html/admin/manager.gohtml +1 -1
| @@ -9,7 +9,7 @@ | |||
| 9 | 9 | <div class="flex-1 flex flex-col space-y-4 p-4 md:ml-4"> | |
| 10 | 10 | <div class="flex flex-col md:flex-row w-full flex-grow md:mt-24"> | |
| 11 | 11 | <div class="flex-grow bg-white p-4 rounded-md shadow mb-4"> | |
| 12 | - | <h1 class="text-lg md:text-2xl font-bold">Container Manager</h1> | |
| 12 | + | <h1 class="text-lg md:text-2xl font-bold">Container Engine Manager</h1> | |
| 13 | 13 | <!-- List all the containers --> | |
| 14 | 14 | <div | |
| 15 | 15 | id="container-manager" | |
| @@ -9,7 +9,7 @@ | |||||
| 9 | <div class="flex-1 flex flex-col space-y-4 p-4 md:ml-4"> | 9 | <div class="flex-1 flex flex-col space-y-4 p-4 md:ml-4"> | ||
| 10 | <div class="flex flex-col md:flex-row w-full flex-grow md:mt-24"> | 10 | <div class="flex flex-col md:flex-row w-full flex-grow md:mt-24"> | ||
| 11 | <div class="flex-grow bg-white p-4 rounded-md shadow mb-4"> | 11 | <div class="flex-grow bg-white p-4 rounded-md shadow mb-4"> | ||
| 12 | - | <h1 class="text-lg md:text-2xl font-bold">Container Manager</h1> | 12 | + | <h1 class="text-lg md:text-2xl font-bold">Container Engine Manager</h1> |
| 13 | <!-- List all the containers --> | 13 | <!-- List all the containers --> | ||
| 14 | <div | 14 | <div | ||
| 15 | id="container-manager" | 15 | id="container-manager" | ||
M internal/gotemplate/models/html/fragments/containerlist.gohtml +2 -2
| @@ -1,5 +1,5 @@ | |||
| 1 | 1 | <div class="flex flex-col p-4"> | |
| 2 | - | <h1 class="text-lg md:text-xl font-bold">Running containers</h1> | |
| 2 | + | <h1 class="text-lg md:text-xl font-bold">Containers</h1> | |
| 3 | 3 | <table class="p-4 border-t-2" hx-ext="response-targets"> | |
| 4 | 4 | <!-- <thead> | |
| 5 | 5 | <tr> | |
| @@ -73,7 +73,7 @@ | |||
| 73 | 73 | </tr> | |
| 74 | 74 | {{ else }} | |
| 75 | 75 | <tr> | |
| 76 | - | <td colspan="7" class="mt-2">No Running containers.</td> | |
| 76 | + | <td colspan="7" class="mt-2">No available containers.</td> | |
| 77 | 77 | </tr> | |
| 78 | 78 | {{ | |
| 79 | 79 | end | |
| @@ -1,5 +1,5 @@ | |||||
| 1 | <div class="flex flex-col p-4"> | 1 | <div class="flex flex-col p-4"> | ||
| 2 | - | <h1 class="text-lg md:text-xl font-bold">Running containers</h1> | 2 | + | <h1 class="text-lg md:text-xl font-bold">Containers</h1> |
| 3 | <table class="p-4 border-t-2" hx-ext="response-targets"> | 3 | <table class="p-4 border-t-2" hx-ext="response-targets"> | ||
| 4 | <!-- <thead> | 4 | <!-- <thead> | ||
| 5 | <tr> | 5 | <tr> | ||
| @@ -73,7 +73,7 @@ | |||||
| 73 | </tr> | 73 | </tr> | ||
| 74 | {{ else }} | 74 | {{ else }} | ||
| 75 | <tr> | 75 | <tr> | ||
| 76 | - | <td colspan="7" class="mt-2">No Running containers.</td> | 76 | + | <td colspan="7" class="mt-2">No available containers.</td> |
| 77 | </tr> | 77 | </tr> | ||
| 78 | {{ | 78 | {{ | ||
| 79 | end | 79 | end | ||
A internal/gotemplate/models/html/fragments/createcontainer.gohtml +82 -0
| @@ -0,0 +1,82 @@ | |||
| 1 | + | <div | |
| 2 | + | class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" | |
| 3 | + | > | |
| 4 | + | <!-- Left Side: Docker Run Options --> | |
| 5 | + | <div class="flex-grow bg-slate-100 p-4 rounded-md"> | |
| 6 | + | <h2 class="font-bold pb-4">Deploy a container based on {{.ImageName}}</h2> | |
| 7 | + | {{if .ErrorMessage}} | |
| 8 | + | <div class="text-red-500">{{.ErrorMessage}}</div> | |
| 9 | + | {{ end }} | |
| 10 | + | <form | |
| 11 | + | action="/create-container" | |
| 12 | + | method="post" | |
| 13 | + | hx-post="/htmx/create-container" | |
| 14 | + | hx-swap="innerHTML" | |
| 15 | + | > | |
| 16 | + | <!-- Container Name --> | |
| 17 | + | <div class="mb-4"> | |
| 18 | + | <label class="block">Container name</label> | |
| 19 | + | <input | |
| 20 | + | type="text" | |
| 21 | + | name="container_name" | |
| 22 | + | class="w-full p-4 rounded-md" | |
| 23 | + | value="{{.ContainerName}}" | |
| 24 | + | /> | |
| 25 | + | </div> | |
| 26 | + | <!-- Domain/Subdomain --> | |
| 27 | + | <div class="mb-4"> | |
| 28 | + | <label class="block">Domain/Subdomain</label> | |
| 29 | + | <input | |
| 30 | + | type="text" | |
| 31 | + | name="container_name" | |
| 32 | + | class="w-full p-4 rounded-md" | |
| 33 | + | value="{{.ContainerDomain}}" | |
| 34 | + | /> | |
| 35 | + | </div> | |
| 36 | + | ||
| 37 | + | <!-- Image Name --> | |
| 38 | + | <div class="mb-4"> | |
| 39 | + | <label class="block">Image name</label> | |
| 40 | + | <input | |
| 41 | + | type="text" | |
| 42 | + | name="image_name" | |
| 43 | + | class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2" | |
| 44 | + | value="{{.ImageName}}" | |
| 45 | + | readonly | |
| 46 | + | /> | |
| 47 | + | </div> | |
| 48 | + | ||
| 49 | + | <!-- Ports --> | |
| 50 | + | <div class="mb-4"> | |
| 51 | + | <label class="block">Ports</label> | |
| 52 | + | <span class="text-gray-500" | |
| 53 | + | >e.g. 80:80 (left is host, right is container)</span | |
| 54 | + | > | |
| 55 | + | <input | |
| 56 | + | type="text" | |
| 57 | + | name="ports" | |
| 58 | + | class="w-full p-4 rounded-md" | |
| 59 | + | value="{{.Ports}}" | |
| 60 | + | /> | |
| 61 | + | </div> | |
| 62 | + | ||
| 63 | + | <!-- Volumes --> | |
| 64 | + | <div class="mb-4"> | |
| 65 | + | <label class="block">Volumes</label> | |
| 66 | + | <span class="text-gray-500">e.g. ./data:/container/data </span> | |
| 67 | + | <input | |
| 68 | + | type="text" | |
| 69 | + | name="volumes" | |
| 70 | + | class="w-full p-4 rounded-md" | |
| 71 | + | value="{{.Volumes}}" | |
| 72 | + | /> | |
| 73 | + | </div> | |
| 74 | + | ||
| 75 | + | <input | |
| 76 | + | type="submit" | |
| 77 | + | value="Create" | |
| 78 | + | class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" | |
| 79 | + | /> | |
| 80 | + | </form> | |
| 81 | + | </div> | |
| 82 | + | </div> | |
| @@ -0,0 +1,82 @@ | |||||
| 1 | + | <div | |||
| 2 | + | class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" | |||
| 3 | + | > | |||
| 4 | + | <!-- Left Side: Docker Run Options --> | |||
| 5 | + | <div class="flex-grow bg-slate-100 p-4 rounded-md"> | |||
| 6 | + | <h2 class="font-bold pb-4">Deploy a container based on {{.ImageName}}</h2> | |||
| 7 | + | {{if .ErrorMessage}} | |||
| 8 | + | <div class="text-red-500">{{.ErrorMessage}}</div> | |||
| 9 | + | {{ end }} | |||
| 10 | + | <form | |||
| 11 | + | action="/create-container" | |||
| 12 | + | method="post" | |||
| 13 | + | hx-post="/htmx/create-container" | |||
| 14 | + | hx-swap="innerHTML" | |||
| 15 | + | > | |||
| 16 | + | <!-- Container Name --> | |||
| 17 | + | <div class="mb-4"> | |||
| 18 | + | <label class="block">Container name</label> | |||
| 19 | + | <input | |||
| 20 | + | type="text" | |||
| 21 | + | name="container_name" | |||
| 22 | + | class="w-full p-4 rounded-md" | |||
| 23 | + | value="{{.ContainerName}}" | |||
| 24 | + | /> | |||
| 25 | + | </div> | |||
| 26 | + | <!-- Domain/Subdomain --> | |||
| 27 | + | <div class="mb-4"> | |||
| 28 | + | <label class="block">Domain/Subdomain</label> | |||
| 29 | + | <input | |||
| 30 | + | type="text" | |||
| 31 | + | name="container_name" | |||
| 32 | + | class="w-full p-4 rounded-md" | |||
| 33 | + | value="{{.ContainerDomain}}" | |||
| 34 | + | /> | |||
| 35 | + | </div> | |||
| 36 | + | ||||
| 37 | + | <!-- Image Name --> | |||
| 38 | + | <div class="mb-4"> | |||
| 39 | + | <label class="block">Image name</label> | |||
| 40 | + | <input | |||
| 41 | + | type="text" | |||
| 42 | + | name="image_name" | |||
| 43 | + | class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2" | |||
| 44 | + | value="{{.ImageName}}" | |||
| 45 | + | readonly | |||
| 46 | + | /> | |||
| 47 | + | </div> | |||
| 48 | + | ||||
| 49 | + | <!-- Ports --> | |||
| 50 | + | <div class="mb-4"> | |||
| 51 | + | <label class="block">Ports</label> | |||
| 52 | + | <span class="text-gray-500" | |||
| 53 | + | >e.g. 80:80 (left is host, right is container)</span | |||
| 54 | + | > | |||
| 55 | + | <input | |||
| 56 | + | type="text" | |||
| 57 | + | name="ports" | |||
| 58 | + | class="w-full p-4 rounded-md" | |||
| 59 | + | value="{{.Ports}}" | |||
| 60 | + | /> | |||
| 61 | + | </div> | |||
| 62 | + | ||||
| 63 | + | <!-- Volumes --> | |||
| 64 | + | <div class="mb-4"> | |||
| 65 | + | <label class="block">Volumes</label> | |||
| 66 | + | <span class="text-gray-500">e.g. ./data:/container/data </span> | |||
| 67 | + | <input | |||
| 68 | + | type="text" | |||
| 69 | + | name="volumes" | |||
| 70 | + | class="w-full p-4 rounded-md" | |||
| 71 | + | value="{{.Volumes}}" | |||
| 72 | + | /> | |||
| 73 | + | </div> | |||
| 74 | + | ||||
| 75 | + | <input | |||
| 76 | + | type="submit" | |||
| 77 | + | value="Create" | |||
| 78 | + | class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" | |||
| 79 | + | /> | |||
| 80 | + | </form> | |||
| 81 | + | </div> | |||
| 82 | + | </div> | |||
M internal/gotemplate/models/html/fragments/imagelist.gohtml +14 -1
| @@ -35,6 +35,18 @@ | |||
| 35 | 35 | </td> | |
| 36 | 36 | <td>{{.SizeStr}}</td> | |
| 37 | 37 | <td align="right"> | |
| 38 | + | ||
| 39 | + | <button | |
| 40 | + | id="add-button-img-{{.ID}}" | |
| 41 | + | hx-get="/htmx/create-container/{{.ID}}" | |
| 42 | + | hx-target="#create-container" | |
| 43 | + | hx-target-*="#error-message-div-{{.ID}}" | |
| 44 | + | hx-swap="outerHTML" | |
| 45 | + | class="py-2 px-4" | |
| 46 | + | > | |
| 47 | + | <span title="Deploy a container using this image" class="iconoir-add-circle text-xl text-blue-800 font-bold"></span> | |
| 48 | + | </button> | |
| 49 | + | ||
| 38 | 50 | <button | |
| 39 | 51 | id="remove-button-img-{{.ID}}" | |
| 40 | 52 | hx-delete="/htmx/image-manager/delete/{{.ID}}" | |
| @@ -44,7 +56,7 @@ | |||
| 44 | 56 | hx-confirm="Are you sure you want to delete this container image?" | |
| 45 | 57 | class="py-2 px-4" | |
| 46 | 58 | > | |
| 47 | - | <span class="iconoir-trash text-xl text-red-600 font-bold"></span> | |
| 59 | + | <span title="Remove this image" class="iconoir-trash text-xl text-red-600 font-bold"></span> | |
| 48 | 60 | </button> | |
| 49 | 61 | </td> | |
| 50 | 62 | </tr> | |
| @@ -63,4 +75,5 @@ | |||
| 63 | 75 | </tbody> | |
| 64 | 76 | </table> | |
| 65 | 77 | <div id="upload-image"></div> | |
| 78 | + | <div id="create-container"></div> | |
| 66 | 79 | </div> | |
| @@ -35,6 +35,18 @@ | |||||
| 35 | </td> | 35 | </td> | ||
| 36 | <td>{{.SizeStr}}</td> | 36 | <td>{{.SizeStr}}</td> | ||
| 37 | <td align="right"> | 37 | <td align="right"> | ||
| 38 | + | ||||
| 39 | + | <button | |||
| 40 | + | id="add-button-img-{{.ID}}" | |||
| 41 | + | hx-get="/htmx/create-container/{{.ID}}" | |||
| 42 | + | hx-target="#create-container" | |||
| 43 | + | hx-target-*="#error-message-div-{{.ID}}" | |||
| 44 | + | hx-swap="outerHTML" | |||
| 45 | + | class="py-2 px-4" | |||
| 46 | + | > | |||
| 47 | + | <span title="Deploy a container using this image" class="iconoir-add-circle text-xl text-blue-800 font-bold"></span> | |||
| 48 | + | </button> | |||
| 49 | + | ||||
| 38 | <button | 50 | <button | ||
| 39 | id="remove-button-img-{{.ID}}" | 51 | id="remove-button-img-{{.ID}}" | ||
| 40 | hx-delete="/htmx/image-manager/delete/{{.ID}}" | 52 | hx-delete="/htmx/image-manager/delete/{{.ID}}" | ||
| @@ -44,7 +56,7 @@ | |||||
| 44 | hx-confirm="Are you sure you want to delete this container image?" | 56 | hx-confirm="Are you sure you want to delete this container image?" | ||
| 45 | class="py-2 px-4" | 57 | class="py-2 px-4" | ||
| 46 | > | 58 | > | ||
| 47 | - | <span class="iconoir-trash text-xl text-red-600 font-bold"></span> | 59 | + | <span title="Remove this image" class="iconoir-trash text-xl text-red-600 font-bold"></span> |
| 48 | </button> | 60 | </button> | ||
| 49 | </td> | 61 | </td> | ||
| 50 | </tr> | 62 | </tr> | ||
| @@ -63,4 +75,5 @@ | |||||
| 63 | </tbody> | 75 | </tbody> | ||
| 64 | </table> | 76 | </table> | ||
| 65 | <div id="upload-image"></div> | 77 | <div id="upload-image"></div> | ||
| 78 | + | <div id="create-container"></div> | |||
| 66 | </div> | 79 | </div> | ||
M internal/gotemplate/models/html/fragments/uploadimage.gohtml +3 -3
| @@ -1,10 +1,10 @@ | |||
| 1 | - | <!-- Form to upload a container image (.tar.gz format) --> | |
| 1 | + | <!-- Form to upload a container image (.tar format) --> | |
| 2 | 2 | <div | |
| 3 | 3 | class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" | |
| 4 | 4 | hx-ext="response-targets" | |
| 5 | 5 | hx-boost="true" | |
| 6 | 6 | > | |
| 7 | - | <div class="flex-grow bg-white p-4 rounded-md shadow mb-4"> | |
| 7 | + | <div class="flex-grow bg-slate-100 p-4 rounded-md"> | |
| 8 | 8 | <h2>Upload Image Containers</h2> | |
| 9 | 9 | <!-- Upload Form --> | |
| 10 | 10 | <form | |
| @@ -17,7 +17,7 @@ | |||
| 17 | 17 | hx-target-*="#error-message-div" | |
| 18 | 18 | > | |
| 19 | 19 | <div | |
| 20 | - | class="upload-zone w-full h-44 bg-gray-300 rounded-md shadow mb-4" | |
| 20 | + | class="upload-zone w-full h-44 bg-slate-200 rounded-md p-4" | |
| 21 | 21 | hx-drop="true" | |
| 22 | 22 | > | |
| 23 | 23 | Drag & Drop your file here or | |
| @@ -1,10 +1,10 @@ | |||||
| 1 | - | <!-- Form to upload a container image (.tar.gz format) --> | 1 | + | <!-- Form to upload a container image (.tar format) --> |
| 2 | <div | 2 | <div | ||
| 3 | class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" | 3 | class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" | ||
| 4 | hx-ext="response-targets" | 4 | hx-ext="response-targets" | ||
| 5 | hx-boost="true" | 5 | hx-boost="true" | ||
| 6 | > | 6 | > | ||
| 7 | - | <div class="flex-grow bg-white p-4 rounded-md shadow mb-4"> | 7 | + | <div class="flex-grow bg-slate-100 p-4 rounded-md"> |
| 8 | <h2>Upload Image Containers</h2> | 8 | <h2>Upload Image Containers</h2> | ||
| 9 | <!-- Upload Form --> | 9 | <!-- Upload Form --> | ||
| 10 | <form | 10 | <form | ||
| @@ -17,7 +17,7 @@ | |||||
| 17 | hx-target-*="#error-message-div" | 17 | hx-target-*="#error-message-div" | ||
| 18 | > | 18 | > | ||
| 19 | <div | 19 | <div | ||
| 20 | - | class="upload-zone w-full h-44 bg-gray-300 rounded-md shadow mb-4" | 20 | + | class="upload-zone w-full h-44 bg-slate-200 rounded-md p-4" |
| 21 | hx-drop="true" | 21 | hx-drop="true" | ||
| 22 | > | 22 | > | ||
| 23 | Drag & Drop your file here or | 23 | Drag & Drop your file here or | ||
A internal/gotemplate/render/strings.go +16 -0
| @@ -0,0 +1,16 @@ | |||
| 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 = "" | |
| @@ -0,0 +1,16 @@ | |||||
| 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 = "" | |||
A internal/httpserve/handler/createcontainer.go +66 -0
| @@ -0,0 +1,66 @@ | |||
| 1 | + | package handler | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "net/http" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/gordon/internal/app" | |
| 7 | + | "github.com/bnema/gordon/internal/gotemplate/render" | |
| 8 | + | "github.com/bnema/gordon/pkg/utils/docker" | |
| 9 | + | "github.com/labstack/echo/v4" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | type CreateContainer struct { | |
| 13 | + | ContainerName string | |
| 14 | + | ContainerHost string | |
| 15 | + | Environment string | |
| 16 | + | ImageName string | |
| 17 | + | ImageID string | |
| 18 | + | Ports string | |
| 19 | + | Volumes string | |
| 20 | + | TraefikLabels []string | |
| 21 | + | Network string | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // CreateContainerRoute is the route for creating a new container | |
| 25 | + | func CreateContainerGETHandler(c echo.Context, a *app.App) error { | |
| 26 | + | // Retreive the image ID from the URL | |
| 27 | + | ShortID := c.Param("ID") | |
| 28 | + | idMapMutex.Lock() | |
| 29 | + | // Check if the ShortImgID exists in the idMap | |
| 30 | + | imageID, exists := idMap[ShortID] | |
| 31 | + | idMapMutex.Unlock() | |
| 32 | + | ||
| 33 | + | imageInfo, err := docker.GetImageInfo(imageID) | |
| 34 | + | if err != nil { | |
| 35 | + | return sendError(c, err) | |
| 36 | + | } | |
| 37 | + | // Extract image name | |
| 38 | + | var imageName string | |
| 39 | + | if len(imageInfo.RepoTags) > 0 { | |
| 40 | + | imageName = imageInfo.RepoTags[0] // Using the first tag as the image name | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | if !exists { | |
| 44 | + | return c.String(http.StatusBadRequest, "Invalid ShortImgID") | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | data := map[string]interface{}{ | |
| 48 | + | "Title": "Create a new container", | |
| 49 | + | "ImageName": imageName, | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | rendererData, err := render.GetHTMLRenderer("html/fragments", "createcontainer.gohtml", a.TemplateFS, a) | |
| 53 | + | ||
| 54 | + | if err != nil { | |
| 55 | + | return sendError(c, err) | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | renderedHTML, err := rendererData.Render(data, a) | |
| 59 | + | ||
| 60 | + | if err != nil { | |
| 61 | + | return sendError(c, err) | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | return c.HTML(200, renderedHTML) | |
| 65 | + | ||
| 66 | + | } | |
| @@ -0,0 +1,66 @@ | |||||
| 1 | + | package handler | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "net/http" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/gordon/internal/app" | |||
| 7 | + | "github.com/bnema/gordon/internal/gotemplate/render" | |||
| 8 | + | "github.com/bnema/gordon/pkg/utils/docker" | |||
| 9 | + | "github.com/labstack/echo/v4" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | type CreateContainer struct { | |||
| 13 | + | ContainerName string | |||
| 14 | + | ContainerHost string | |||
| 15 | + | Environment string | |||
| 16 | + | ImageName string | |||
| 17 | + | ImageID string | |||
| 18 | + | Ports string | |||
| 19 | + | Volumes string | |||
| 20 | + | TraefikLabels []string | |||
| 21 | + | Network string | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // CreateContainerRoute is the route for creating a new container | |||
| 25 | + | func CreateContainerGETHandler(c echo.Context, a *app.App) error { | |||
| 26 | + | // Retreive the image ID from the URL | |||
| 27 | + | ShortID := c.Param("ID") | |||
| 28 | + | idMapMutex.Lock() | |||
| 29 | + | // Check if the ShortImgID exists in the idMap | |||
| 30 | + | imageID, exists := idMap[ShortID] | |||
| 31 | + | idMapMutex.Unlock() | |||
| 32 | + | ||||
| 33 | + | imageInfo, err := docker.GetImageInfo(imageID) | |||
| 34 | + | if err != nil { | |||
| 35 | + | return sendError(c, err) | |||
| 36 | + | } | |||
| 37 | + | // Extract image name | |||
| 38 | + | var imageName string | |||
| 39 | + | if len(imageInfo.RepoTags) > 0 { | |||
| 40 | + | imageName = imageInfo.RepoTags[0] // Using the first tag as the image name | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | if !exists { | |||
| 44 | + | return c.String(http.StatusBadRequest, "Invalid ShortImgID") | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | data := map[string]interface{}{ | |||
| 48 | + | "Title": "Create a new container", | |||
| 49 | + | "ImageName": imageName, | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | rendererData, err := render.GetHTMLRenderer("html/fragments", "createcontainer.gohtml", a.TemplateFS, a) | |||
| 53 | + | ||||
| 54 | + | if err != nil { | |||
| 55 | + | return sendError(c, err) | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | renderedHTML, err := rendererData.Render(data, a) | |||
| 59 | + | ||||
| 60 | + | if err != nil { | |||
| 61 | + | return sendError(c, err) | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | return c.HTML(200, renderedHTML) | |||
| 65 | + | ||||
| 66 | + | } | |||
M internal/httpserve/router.go +3 -0
| @@ -88,4 +88,7 @@ func bindHTMXEndpoints(e *echo.Echo, a *app.App) { | |||
| 88 | 88 | e.POST("/htmx/upload-image", func(c echo.Context) error { | |
| 89 | 89 | return handler.UploadImagePOSTHandler(c, a) | |
| 90 | 90 | }) | |
| 91 | + | e.GET("htmx/create-container/:ID", func(c echo.Context) error { | |
| 92 | + | return handler.CreateContainerGETHandler(c, a) | |
| 93 | + | }) | |
| 91 | 94 | } | |
| @@ -88,4 +88,7 @@ func bindHTMXEndpoints(e *echo.Echo, a *app.App) { | |||||
| 88 | e.POST("/htmx/upload-image", func(c echo.Context) error { | 88 | e.POST("/htmx/upload-image", func(c echo.Context) error { | ||
| 89 | return handler.UploadImagePOSTHandler(c, a) | 89 | return handler.UploadImagePOSTHandler(c, a) | ||
| 90 | }) | 90 | }) | ||
| 91 | + | e.GET("htmx/create-container/:ID", func(c echo.Context) error { | |||
| 92 | + | return handler.CreateContainerGETHandler(c, a) | |||
| 93 | + | }) | |||
| 91 | } | 94 | } | ||
M internal/webui/public/assets/css/tailwind.css +39 -23
| @@ -580,6 +580,10 @@ video { | |||
| 580 | 580 | margin-bottom: 0.5rem; | |
| 581 | 581 | } | |
| 582 | 582 | ||
| 583 | + | .mb-1 { | |
| 584 | + | margin-bottom: 0.25rem; | |
| 585 | + | } | |
| 586 | + | ||
| 583 | 587 | .mb-4 { | |
| 584 | 588 | margin-bottom: 1rem; | |
| 585 | 589 | } | |
| @@ -600,16 +604,8 @@ video { | |||
| 600 | 604 | margin-top: 1rem; | |
| 601 | 605 | } | |
| 602 | 606 | ||
| 603 | - | .mb-1 { | |
| 604 | - | margin-bottom: 0.25rem; | |
| 605 | - | } | |
| 606 | - | ||
| 607 | - | .mt-1 { | |
| 608 | - | margin-top: 0.25rem; | |
| 609 | - | } | |
| 610 | - | ||
| 611 | - | .mt-3 { | |
| 612 | - | margin-top: 0.75rem; | |
| 607 | + | .block { | |
| 608 | + | display: block; | |
| 613 | 609 | } | |
| 614 | 610 | ||
| 615 | 611 | .flex { | |
| @@ -741,6 +737,10 @@ video { | |||
| 741 | 737 | border-width: 1px; | |
| 742 | 738 | } | |
| 743 | 739 | ||
| 740 | + | .border-2 { | |
| 741 | + | border-width: 2px; | |
| 742 | + | } | |
| 743 | + | ||
| 744 | 744 | .border-t-2 { | |
| 745 | 745 | border-top-width: 2px; | |
| 746 | 746 | } | |
| @@ -765,11 +765,26 @@ video { | |||
| 765 | 765 | background-color: rgb(0 123 192 / var(--tw-bg-opacity)); | |
| 766 | 766 | } | |
| 767 | 767 | ||
| 768 | + | .bg-gray-100 { | |
| 769 | + | --tw-bg-opacity: 1; | |
| 770 | + | background-color: rgb(243 244 246 / var(--tw-bg-opacity)); | |
| 771 | + | } | |
| 772 | + | ||
| 768 | 773 | .bg-gray-300 { | |
| 769 | 774 | --tw-bg-opacity: 1; | |
| 770 | 775 | background-color: rgb(209 213 219 / var(--tw-bg-opacity)); | |
| 771 | 776 | } | |
| 772 | 777 | ||
| 778 | + | .bg-slate-100 { | |
| 779 | + | --tw-bg-opacity: 1; | |
| 780 | + | background-color: rgb(241 245 249 / var(--tw-bg-opacity)); | |
| 781 | + | } | |
| 782 | + | ||
| 783 | + | .bg-slate-200 { | |
| 784 | + | --tw-bg-opacity: 1; | |
| 785 | + | background-color: rgb(226 232 240 / var(--tw-bg-opacity)); | |
| 786 | + | } | |
| 787 | + | ||
| 773 | 788 | .bg-transparent { | |
| 774 | 789 | background-color: transparent; | |
| 775 | 790 | } | |
| @@ -806,22 +821,18 @@ video { | |||
| 806 | 821 | padding-right: 1rem; | |
| 807 | 822 | } | |
| 808 | 823 | ||
| 809 | - | .py-2 { | |
| 810 | - | padding-top: 0.5rem; | |
| 811 | - | padding-bottom: 0.5rem; | |
| 812 | - | } | |
| 813 | - | ||
| 814 | 824 | .py-1 { | |
| 815 | 825 | padding-top: 0.25rem; | |
| 816 | 826 | padding-bottom: 0.25rem; | |
| 817 | 827 | } | |
| 818 | 828 | ||
| 819 | - | .pt-2 { | |
| 829 | + | .py-2 { | |
| 820 | 830 | padding-top: 0.5rem; | |
| 831 | + | padding-bottom: 0.5rem; | |
| 821 | 832 | } | |
| 822 | 833 | ||
| 823 | - | .align-baseline { | |
| 824 | - | vertical-align: baseline; | |
| 834 | + | .pb-4 { | |
| 835 | + | padding-bottom: 1rem; | |
| 825 | 836 | } | |
| 826 | 837 | ||
| 827 | 838 | .align-middle { | |
| @@ -870,6 +881,11 @@ video { | |||
| 870 | 881 | letter-spacing: -0.05em; | |
| 871 | 882 | } | |
| 872 | 883 | ||
| 884 | + | .text-blue-800 { | |
| 885 | + | --tw-text-opacity: 1; | |
| 886 | + | color: rgb(30 64 175 / var(--tw-text-opacity)); | |
| 887 | + | } | |
| 888 | + | ||
| 873 | 889 | .text-gordonmascot-hblue { | |
| 874 | 890 | --tw-text-opacity: 1; | |
| 875 | 891 | color: rgb(1 30 92 / var(--tw-text-opacity)); | |
| @@ -885,6 +901,11 @@ video { | |||
| 885 | 901 | color: rgb(243 244 246 / var(--tw-text-opacity)); | |
| 886 | 902 | } | |
| 887 | 903 | ||
| 904 | + | .text-gray-500 { | |
| 905 | + | --tw-text-opacity: 1; | |
| 906 | + | color: rgb(107 114 128 / var(--tw-text-opacity)); | |
| 907 | + | } | |
| 908 | + | ||
| 888 | 909 | .text-gray-800 { | |
| 889 | 910 | --tw-text-opacity: 1; | |
| 890 | 911 | color: rgb(31 41 55 / var(--tw-text-opacity)); | |
| @@ -910,11 +931,6 @@ video { | |||
| 910 | 931 | color: rgb(255 255 255 / var(--tw-text-opacity)); | |
| 911 | 932 | } | |
| 912 | 933 | ||
| 913 | - | .text-black { | |
| 914 | - | --tw-text-opacity: 1; | |
| 915 | - | color: rgb(0 0 0 / var(--tw-text-opacity)); | |
| 916 | - | } | |
| 917 | - | ||
| 918 | 934 | .underline { | |
| 919 | 935 | text-decoration-line: underline; | |
| 920 | 936 | } | |
| @@ -580,6 +580,10 @@ video { | |||||
| 580 | margin-bottom: 0.5rem; | 580 | margin-bottom: 0.5rem; | ||
| 581 | } | 581 | } | ||
| 582 | 582 | ||||
| 583 | + | .mb-1 { | |||
| 584 | + | margin-bottom: 0.25rem; | |||
| 585 | + | } | |||
| 586 | + | ||||
| 583 | .mb-4 { | 587 | .mb-4 { | ||
| 584 | margin-bottom: 1rem; | 588 | margin-bottom: 1rem; | ||
| 585 | } | 589 | } | ||
| @@ -600,16 +604,8 @@ video { | |||||
| 600 | margin-top: 1rem; | 604 | margin-top: 1rem; | ||
| 601 | } | 605 | } | ||
| 602 | 606 | ||||
| 603 | - | .mb-1 { | 607 | + | .block { |
| 604 | - | margin-bottom: 0.25rem; | 608 | + | display: block; |
| 605 | - | } | |||
| 606 | - | ||||
| 607 | - | .mt-1 { | |||
| 608 | - | margin-top: 0.25rem; | |||
| 609 | - | } | |||
| 610 | - | ||||
| 611 | - | .mt-3 { | |||
| 612 | - | margin-top: 0.75rem; | |||
| 613 | } | 609 | } | ||
| 614 | 610 | ||||
| 615 | .flex { | 611 | .flex { | ||
| @@ -741,6 +737,10 @@ video { | |||||
| 741 | border-width: 1px; | 737 | border-width: 1px; | ||
| 742 | } | 738 | } | ||
| 743 | 739 | ||||
| 740 | + | .border-2 { | |||
| 741 | + | border-width: 2px; | |||
| 742 | + | } | |||
| 743 | + | ||||
| 744 | .border-t-2 { | 744 | .border-t-2 { | ||
| 745 | border-top-width: 2px; | 745 | border-top-width: 2px; | ||
| 746 | } | 746 | } | ||
| @@ -765,11 +765,26 @@ video { | |||||
| 765 | background-color: rgb(0 123 192 / var(--tw-bg-opacity)); | 765 | background-color: rgb(0 123 192 / var(--tw-bg-opacity)); | ||
| 766 | } | 766 | } | ||
| 767 | 767 | ||||
| 768 | + | .bg-gray-100 { | |||
| 769 | + | --tw-bg-opacity: 1; | |||
| 770 | + | background-color: rgb(243 244 246 / var(--tw-bg-opacity)); | |||
| 771 | + | } | |||
| 772 | + | ||||
| 768 | .bg-gray-300 { | 773 | .bg-gray-300 { | ||
| 769 | --tw-bg-opacity: 1; | 774 | --tw-bg-opacity: 1; | ||
| 770 | background-color: rgb(209 213 219 / var(--tw-bg-opacity)); | 775 | background-color: rgb(209 213 219 / var(--tw-bg-opacity)); | ||
| 771 | } | 776 | } | ||
| 772 | 777 | ||||
| 778 | + | .bg-slate-100 { | |||
| 779 | + | --tw-bg-opacity: 1; | |||
| 780 | + | background-color: rgb(241 245 249 / var(--tw-bg-opacity)); | |||
| 781 | + | } | |||
| 782 | + | ||||
| 783 | + | .bg-slate-200 { | |||
| 784 | + | --tw-bg-opacity: 1; | |||
| 785 | + | background-color: rgb(226 232 240 / var(--tw-bg-opacity)); | |||
| 786 | + | } | |||
| 787 | + | ||||
| 773 | .bg-transparent { | 788 | .bg-transparent { | ||
| 774 | background-color: transparent; | 789 | background-color: transparent; | ||
| 775 | } | 790 | } | ||
| @@ -806,22 +821,18 @@ video { | |||||
| 806 | padding-right: 1rem; | 821 | padding-right: 1rem; | ||
| 807 | } | 822 | } | ||
| 808 | 823 | ||||
| 809 | - | .py-2 { | |||
| 810 | - | padding-top: 0.5rem; | |||
| 811 | - | padding-bottom: 0.5rem; | |||
| 812 | - | } | |||
| 813 | - | ||||
| 814 | .py-1 { | 824 | .py-1 { | ||
| 815 | padding-top: 0.25rem; | 825 | padding-top: 0.25rem; | ||
| 816 | padding-bottom: 0.25rem; | 826 | padding-bottom: 0.25rem; | ||
| 817 | } | 827 | } | ||
| 818 | 828 | ||||
| 819 | - | .pt-2 { | 829 | + | .py-2 { |
| 820 | padding-top: 0.5rem; | 830 | padding-top: 0.5rem; | ||
| 831 | + | padding-bottom: 0.5rem; | |||
| 821 | } | 832 | } | ||
| 822 | 833 | ||||
| 823 | - | .align-baseline { | 834 | + | .pb-4 { |
| 824 | - | vertical-align: baseline; | 835 | + | padding-bottom: 1rem; |
| 825 | } | 836 | } | ||
| 826 | 837 | ||||
| 827 | .align-middle { | 838 | .align-middle { | ||
| @@ -870,6 +881,11 @@ video { | |||||
| 870 | letter-spacing: -0.05em; | 881 | letter-spacing: -0.05em; | ||
| 871 | } | 882 | } | ||
| 872 | 883 | ||||
| 884 | + | .text-blue-800 { | |||
| 885 | + | --tw-text-opacity: 1; | |||
| 886 | + | color: rgb(30 64 175 / var(--tw-text-opacity)); | |||
| 887 | + | } | |||
| 888 | + | ||||
| 873 | .text-gordonmascot-hblue { | 889 | .text-gordonmascot-hblue { | ||
| 874 | --tw-text-opacity: 1; | 890 | --tw-text-opacity: 1; | ||
| 875 | color: rgb(1 30 92 / var(--tw-text-opacity)); | 891 | color: rgb(1 30 92 / var(--tw-text-opacity)); | ||
| @@ -885,6 +901,11 @@ video { | |||||
| 885 | color: rgb(243 244 246 / var(--tw-text-opacity)); | 901 | color: rgb(243 244 246 / var(--tw-text-opacity)); | ||
| 886 | } | 902 | } | ||
| 887 | 903 | ||||
| 904 | + | .text-gray-500 { | |||
| 905 | + | --tw-text-opacity: 1; | |||
| 906 | + | color: rgb(107 114 128 / var(--tw-text-opacity)); | |||
| 907 | + | } | |||
| 908 | + | ||||
| 888 | .text-gray-800 { | 909 | .text-gray-800 { | ||
| 889 | --tw-text-opacity: 1; | 910 | --tw-text-opacity: 1; | ||
| 890 | color: rgb(31 41 55 / var(--tw-text-opacity)); | 911 | color: rgb(31 41 55 / var(--tw-text-opacity)); | ||
| @@ -910,11 +931,6 @@ video { | |||||
| 910 | color: rgb(255 255 255 / var(--tw-text-opacity)); | 931 | color: rgb(255 255 255 / var(--tw-text-opacity)); | ||
| 911 | } | 932 | } | ||
| 912 | 933 | ||||
| 913 | - | .text-black { | |||
| 914 | - | --tw-text-opacity: 1; | |||
| 915 | - | color: rgb(0 0 0 / var(--tw-text-opacity)); | |||
| 916 | - | } | |||
| 917 | - | ||||
| 918 | .underline { | 934 | .underline { | ||
| 919 | text-decoration-line: underline; | 935 | text-decoration-line: underline; | ||
| 920 | } | 936 | } | ||
M internal/webui/public/assets/js/custom.js +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | document.body.addEventListener("htmx:afterOnLoad", function(event) { | |
| 2 | + | if ( | |
| 3 | + | event.target.id === "image-manager" && | |
| 4 | + | event.detail.xhr.responseText === "Success" | |
| 5 | + | ) { | |
| 6 | + | setTimeout(function() { | |
| 7 | + | location.reload(); | |
| 8 | + | }, 3000); | |
| 9 | + | } | |
| 10 | + | }); | |
| @@ -0,0 +1,10 @@ | |||||
| 1 | + | document.body.addEventListener("htmx:afterOnLoad", function(event) { | |||
| 2 | + | if ( | |||
| 3 | + | event.target.id === "image-manager" && | |||
| 4 | + | event.detail.xhr.responseText === "Success" | |||
| 5 | + | ) { | |||
| 6 | + | setTimeout(function() { | |||
| 7 | + | location.reload(); | |||
| 8 | + | }, 3000); | |||
| 9 | + | } | |||
| 10 | + | }); | |||
M pkg/utils/docker/images.go +11 -2
| @@ -33,8 +33,6 @@ func DeleteContainerImage(imageID string) error { | |||
| 33 | 33 | } | |
| 34 | 34 | ||
| 35 | 35 | func ImportImageToEngine(imagePath string) (string, error) { | |
| 36 | - | fmt.Println("Importing image to Docker engine") | |
| 37 | - | fmt.Println(imagePath) | |
| 38 | 36 | // Check if the Docker client has been initialized | |
| 39 | 37 | CheckIfInitialized() | |
| 40 | 38 | ||
| @@ -77,3 +75,14 @@ func ImportImageToEngine(imagePath string) (string, error) { | |||
| 77 | 75 | ||
| 78 | 76 | return imageID, nil | |
| 79 | 77 | } | |
| 78 | + | ||
| 79 | + | // From an ID, get the all the information about the image | |
| 80 | + | func GetImageInfo(imageID string) (*types.ImageInspect, error) { | |
| 81 | + | // Get the image information using the Docker client | |
| 82 | + | imageInfo, _, err := dockerCli.ImageInspectWithRaw(context.Background(), imageID) | |
| 83 | + | if err != nil { | |
| 84 | + | return nil, err | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | return &imageInfo, nil | |
| 88 | + | } | |
| @@ -33,8 +33,6 @@ func DeleteContainerImage(imageID string) error { | |||||
| 33 | } | 33 | } | ||
| 34 | 34 | ||||
| 35 | func ImportImageToEngine(imagePath string) (string, error) { | 35 | func ImportImageToEngine(imagePath string) (string, error) { | ||
| 36 | - | fmt.Println("Importing image to Docker engine") | |||
| 37 | - | fmt.Println(imagePath) | |||
| 38 | // Check if the Docker client has been initialized | 36 | // Check if the Docker client has been initialized | ||
| 39 | CheckIfInitialized() | 37 | CheckIfInitialized() | ||
| 40 | 38 | ||||
| @@ -77,3 +75,14 @@ func ImportImageToEngine(imagePath string) (string, error) { | |||||
| 77 | 75 | ||||
| 78 | return imageID, nil | 76 | return imageID, nil | ||
| 79 | } | 77 | } | ||
| 78 | + | ||||
| 79 | + | // From an ID, get the all the information about the image | |||
| 80 | + | func GetImageInfo(imageID string) (*types.ImageInspect, error) { | |||
| 81 | + | // Get the image information using the Docker client | |||
| 82 | + | imageInfo, _, err := dockerCli.ImageInspectWithRaw(context.Background(), imageID) | |||
| 83 | + | if err != nil { | |||
| 84 | + | return nil, err | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | return &imageInfo, nil | |||
| 88 | + | } | |||