New view for the manager + some refactoring
For containers: - Add a view to list all the containiers - Start/Stop/Remove a container - Error handling For the images : - List all available images - Remove button - Error handling
- commit
- 4c74d60ca5339ceefac75fd86c3d1005a818fa4e
- author
- bnema · · 2023-09-22 14:38 UTC
- committer
- bnema · · 2023-09-22 14:46 UTC
- parent
- 8771253
- signature
- unsigned
13 files · +346 -207
M cmd/srv/main.go +1 -1
| @@ -35,7 +35,7 @@ func main() { | |||
| 35 | 35 | dockerClient := &docker.DockerClient{} | |
| 36 | 36 | err = dockerClient.InitializeClient(a.Config.NewDockerConfig()) | |
| 37 | 37 | if err != nil { | |
| 38 | - | log.Fatalf("Error initializing Docker client: %s", err) | |
| 38 | + | log.Printf("Error from DockerClient: %s", err) | |
| 39 | 39 | } | |
| 40 | 40 | // Setup a channel to capture termination signals | |
| 41 | 41 | sigs := make(chan os.Signal, 1) | |
| @@ -35,7 +35,7 @@ func main() { | |||||
| 35 | dockerClient := &docker.DockerClient{} | 35 | dockerClient := &docker.DockerClient{} | ||
| 36 | err = dockerClient.InitializeClient(a.Config.NewDockerConfig()) | 36 | err = dockerClient.InitializeClient(a.Config.NewDockerConfig()) | ||
| 37 | if err != nil { | 37 | if err != nil { | ||
| 38 | - | log.Fatalf("Error initializing Docker client: %s", err) | 38 | + | log.Printf("Error from DockerClient: %s", err) |
| 39 | } | 39 | } | ||
| 40 | // Setup a channel to capture termination signals | 40 | // Setup a channel to capture termination signals | ||
| 41 | sigs := make(chan os.Signal, 1) | 41 | sigs := make(chan os.Signal, 1) | ||
M internal/app/app.go +11 -3
| @@ -59,8 +59,9 @@ type AdminConfig struct { | |||
| 59 | 59 | } | |
| 60 | 60 | ||
| 61 | 61 | type ContainerEngineConfig struct { | |
| 62 | - | Sock string `yaml:"sock"` | |
| 63 | - | PodmanEnable bool `yaml:"podman"` | |
| 62 | + | Sock string `yaml:"dockersock"` | |
| 63 | + | PodmanSock string `yaml:"podmansock"` | |
| 64 | + | Podman bool `yaml:"podman"` | |
| 64 | 65 | } | |
| 65 | 66 | ||
| 66 | 67 | func InitializeEnvironment() { | |
| @@ -104,9 +105,16 @@ func NewApp() *App { | |||
| 104 | 105 | ||
| 105 | 106 | // NewDockerConfig creates and returns a new Docker client configuration based on AppConfig. | |
| 106 | 107 | func (config *AppConfig) NewDockerConfig() *docker.Config { | |
| 108 | + | if config.ContainerEngine.Podman { | |
| 109 | + | return &docker.Config{ | |
| 110 | + | Sock: config.ContainerEngine.PodmanSock, | |
| 111 | + | PodmanEnable: true, | |
| 112 | + | } | |
| 113 | + | } | |
| 107 | 114 | return &docker.Config{ | |
| 108 | 115 | Sock: config.ContainerEngine.Sock, | |
| 109 | - | PodmanEnable: config.ContainerEngine.PodmanEnable} | |
| 116 | + | PodmanEnable: false, | |
| 117 | + | } | |
| 110 | 118 | } | |
| 111 | 119 | ||
| 112 | 120 | func GenerateOauthCallbackURL(config AppConfig) string { | |
| @@ -59,8 +59,9 @@ type AdminConfig struct { | |||||
| 59 | } | 59 | } | ||
| 60 | 60 | ||||
| 61 | type ContainerEngineConfig struct { | 61 | type ContainerEngineConfig struct { | ||
| 62 | - | Sock string `yaml:"sock"` | 62 | + | Sock string `yaml:"dockersock"` |
| 63 | - | PodmanEnable bool `yaml:"podman"` | 63 | + | PodmanSock string `yaml:"podmansock"` |
| 64 | + | Podman bool `yaml:"podman"` | |||
| 64 | } | 65 | } | ||
| 65 | 66 | ||||
| 66 | func InitializeEnvironment() { | 67 | func InitializeEnvironment() { | ||
| @@ -104,9 +105,16 @@ func NewApp() *App { | |||||
| 104 | 105 | ||||
| 105 | // NewDockerConfig creates and returns a new Docker client configuration based on AppConfig. | 106 | // NewDockerConfig creates and returns a new Docker client configuration based on AppConfig. | ||
| 106 | func (config *AppConfig) NewDockerConfig() *docker.Config { | 107 | func (config *AppConfig) NewDockerConfig() *docker.Config { | ||
| 108 | + | if config.ContainerEngine.Podman { | |||
| 109 | + | return &docker.Config{ | |||
| 110 | + | Sock: config.ContainerEngine.PodmanSock, | |||
| 111 | + | PodmanEnable: true, | |||
| 112 | + | } | |||
| 113 | + | } | |||
| 107 | return &docker.Config{ | 114 | return &docker.Config{ | ||
| 108 | Sock: config.ContainerEngine.Sock, | 115 | Sock: config.ContainerEngine.Sock, | ||
| 109 | - | PodmanEnable: config.ContainerEngine.PodmanEnable} | 116 | + | PodmanEnable: false, |
| 117 | + | } | |||
| 110 | } | 118 | } | ||
| 111 | 119 | ||||
| 112 | func GenerateOauthCallbackURL(config AppConfig) string { | 120 | func GenerateOauthCallbackURL(config AppConfig) string { | ||
M internal/gotemplate/models/html/admin/manager.gohtml +2 -1
| @@ -6,7 +6,6 @@ | |||
| 6 | 6 | <body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-8 p-0"> | |
| 7 | 7 | {{template "leftmenu.gohtml" .}} | |
| 8 | 8 | <!-- Main content here --> | |
| 9 | - | <!-- This div will contain the error message when something goes wrong --> | |
| 10 | 9 | <div class="flex-1 flex flex-col space-y-4 p-4 md:ml-4"> | |
| 11 | 10 | <div class="flex flex-col md:flex-row w-full flex-grow md:mt-24"> | |
| 12 | 11 | <div class="flex-grow bg-white p-4 rounded-md shadow mb-4"> | |
| @@ -19,6 +18,7 @@ | |||
| 19 | 18 | hx-swap="innerHTML" | |
| 20 | 19 | hx-swap-oob="true" | |
| 21 | 20 | hx-target-error="container-manager-error" | |
| 21 | + | hx-poll="5s" | |
| 22 | 22 | ></div> | |
| 23 | 23 | <div id="container-manager-error" style="display: none"></div> | |
| 24 | 24 | <!-- List all the images --> | |
| @@ -29,6 +29,7 @@ | |||
| 29 | 29 | hx-swap="innerHTML" | |
| 30 | 30 | hx-swap-oob="true" | |
| 31 | 31 | hx-target-error="image-manager-error" | |
| 32 | + | hx-poll="5s" | |
| 32 | 33 | ></div> | |
| 33 | 34 | <div id="image-manager-error" style="display: none"></div> | |
| 34 | 35 | </div> | |
| @@ -6,7 +6,6 @@ | |||||
| 6 | <body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-8 p-0"> | 6 | <body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-8 p-0"> | ||
| 7 | {{template "leftmenu.gohtml" .}} | 7 | {{template "leftmenu.gohtml" .}} | ||
| 8 | <!-- Main content here --> | 8 | <!-- Main content here --> | ||
| 9 | - | <!-- This div will contain the error message when something goes wrong --> | |||
| 10 | <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"> | ||
| 11 | <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"> | ||
| 12 | <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"> | ||
| @@ -19,6 +18,7 @@ | |||||
| 19 | hx-swap="innerHTML" | 18 | hx-swap="innerHTML" | ||
| 20 | hx-swap-oob="true" | 19 | hx-swap-oob="true" | ||
| 21 | hx-target-error="container-manager-error" | 20 | hx-target-error="container-manager-error" | ||
| 21 | + | hx-poll="5s" | |||
| 22 | ></div> | 22 | ></div> | ||
| 23 | <div id="container-manager-error" style="display: none"></div> | 23 | <div id="container-manager-error" style="display: none"></div> | ||
| 24 | <!-- List all the images --> | 24 | <!-- List all the images --> | ||
| @@ -29,6 +29,7 @@ | |||||
| 29 | hx-swap="innerHTML" | 29 | hx-swap="innerHTML" | ||
| 30 | hx-swap-oob="true" | 30 | hx-swap-oob="true" | ||
| 31 | hx-target-error="image-manager-error" | 31 | hx-target-error="image-manager-error" | ||
| 32 | + | hx-poll="5s" | |||
| 32 | ></div> | 33 | ></div> | ||
| 33 | <div id="image-manager-error" style="display: none"></div> | 34 | <div id="image-manager-error" style="display: none"></div> | ||
| 34 | </div> | 35 | </div> | ||
M internal/gotemplate/models/html/fragments/containerlist.gohtml +80 -43
| @@ -1,46 +1,83 @@ | |||
| 1 | 1 | <div class="flex flex-col p-4"> | |
| 2 | 2 | <h1 class="text-lg md:text-xl font-bold">Running containers</h1> | |
| 3 | - | <div class="flex font-bold border-t-2 align-baseline py-2"> | |
| 4 | - | <div class="w-1/6">Name</div> | |
| 5 | - | <div class="w-1/6">Command</div> | |
| 6 | - | <div class="w-1/6">Created</div> | |
| 7 | - | <div class="w-1/6">Status</div> | |
| 8 | - | <div class="w-1/6">Port</div> | |
| 9 | - | <div class="w-1/6">State</div> | |
| 10 | - | <div class="w-1/4">Action</div> | |
| 11 | - | </div> | |
| 12 | - | {{ range.Containers }} | |
| 13 | - | <div id="{{.ID}}" class="flex py-2 align-baseline justify-center"> | |
| 14 | - | <div class="w-1/6">{{ range.Names }}{{.}}<br />{{ end }}</div> | |
| 15 | - | <div class="w-1/6" style="text-decoration: underline">{{.Command}}</div> | |
| 16 | - | <div class="w-1/6">{{.CreatedStr}}</div> | |
| 17 | - | <div class="w-1/6">{{.Status}} (Up since {{.UpSince}})</div> | |
| 18 | - | <div class="w-1/6">{{ range.Ports }}{{.}}<br />{{ end }}</div> | |
| 19 | - | <div class="w-1/6" style="color: {{.StateColor}};">{{.State}}</div> | |
| 20 | - | <div class="w-1/4"> | |
| 21 | - | <div x-data="{ success: false, loading: false }"> | |
| 22 | - | <button | |
| 23 | - | id="remove-button-{{.ShortID}}" | |
| 24 | - | x-bind:class="{ 'bg-green-500': success, 'bg-red-500': !success, 'text-white': true, 'px-2': true, 'py-1': true, 'rounded': true }" | |
| 25 | - | @click="loading = true" | |
| 26 | - | hx-trigger="click" | |
| 27 | - | hx-delete="/htmx/container-manager/delete/{{.ShortID}}" | |
| 28 | - | hx-swap="outerHTML swap:outerHTML" | |
| 29 | - | hx-target-error="container-delete-images-error" | |
| 30 | - | x-on:htmx:afterSwap="if ($event.detail.status === 200 && $event.detail.xhr.response === 'Success') { success = true; loading = false; setTimeout(() => location.reload(), 3000); }" | |
| 31 | - | > | |
| 32 | - | <span x-show="!loading && !success">Remove</span> | |
| 33 | - | <span x-show="success">Success</span> | |
| 34 | - | </button> | |
| 35 | - | </div> | |
| 36 | - | </div> | |
| 37 | - | </div> | |
| 38 | - | {{ else }} | |
| 39 | - | <div class="mt-2">No Running containers.</div> | |
| 40 | - | {{ end }} | |
| 41 | - | <div | |
| 42 | - | id="container-delete-error" | |
| 43 | - | style="display: none" | |
| 44 | - | class="mt-2 text-red-800 text-lg" | |
| 45 | - | ></div> | |
| 3 | + | <table class="p-4 border-t-2" hx-ext="response-targets"> | |
| 4 | + | <!-- <thead> | |
| 5 | + | <tr> | |
| 6 | + | <th>Name</th> | |
| 7 | + | <th>Command</th> | |
| 8 | + | <th>Created</th> | |
| 9 | + | <th>Status</th> | |
| 10 | + | <th>Port</th> | |
| 11 | + | <th>State</th> | |
| 12 | + | <th>Action</th> | |
| 13 | + | </tr> | |
| 14 | + | </thead> --> | |
| 15 | + | <tbody> | |
| 16 | + | {{ | |
| 17 | + | range.containers | |
| 18 | + | }} | |
| 19 | + | <tr id="container-{{.ID}}"> | |
| 20 | + | <td> | |
| 21 | + | <span title="{{.Container.ID}}">{{.Name}}</span> | |
| 22 | + | </td> | |
| 23 | + | <td class="command-container" style="text-decoration: underline"> | |
| 24 | + | <span title="{{.Command}}">{{.Command}}</span> | |
| 25 | + | </td> | |
| 26 | + | <td>{{.CreatedStr}}</td> | |
| 27 | + | <td class="port-container">{{ .Ports }}</td> | |
| 28 | + | <td class="status-container">{{.Status}} (Up since {{.UpSince}})</td> | |
| 29 | + | <td style="color: {{.StateColor}};">{{.State}}</td> | |
| 30 | + | <td align="right"> | |
| 31 | + | <!-- iconoir start --> | |
| 32 | + | <button | |
| 33 | + | id="start-button-{{.ID}}" | |
| 34 | + | hx-post="/htmx/container-manager/start/{{.ID}}" | |
| 35 | + | hx-target="#container-{{.ID}}" | |
| 36 | + | hx-target-*="#error-message-{{.ID}}" | |
| 37 | + | hx-swap="innerHTML" | |
| 38 | + | class="py-2 px-4" | |
| 39 | + | > | |
| 40 | + | <span class="iconoir-play text-xl font-bold"></span> | |
| 41 | + | </button> | |
| 42 | + | <!-- stop --> | |
| 43 | + | <button | |
| 44 | + | id="stop-button-{{.ID}}" | |
| 45 | + | hx-post="/htmx/container-manager/stop/{{.ID}}" | |
| 46 | + | hx-target="#container-{{.ID}}" | |
| 47 | + | hx-target-*="#error-message-{{.ID}}" | |
| 48 | + | hx-swap="innerHTML" | |
| 49 | + | hx-confirm="Are you sure you want to stop this container?" | |
| 50 | + | class="py-2 px-4" | |
| 51 | + | > | |
| 52 | + | <span class="iconoir-system-shut text-xl font-bold"></span> | |
| 53 | + | </button> | |
| 54 | + | <!-- REMOVE --> | |
| 55 | + | <button | |
| 56 | + | id="remove-button-img-{{.ID}}" | |
| 57 | + | hx-delete="/htmx/container-manager/delete/{{.ID}}" | |
| 58 | + | hx-target="#container-{{.ID}}" | |
| 59 | + | hx-target-*="#error-message-{{.ID}}" | |
| 60 | + | hx-swap="innerHTML" | |
| 61 | + | hx-confirm="Are you sure you want to delete this container image?" | |
| 62 | + | class="py-2 px-4" | |
| 63 | + | > | |
| 64 | + | <!-- iconoir trash --> | |
| 65 | + | <span class="iconoir-trash text-xl text-red-600 font-bold"></span> | |
| 66 | + | </button> | |
| 67 | + | </td> | |
| 68 | + | </tr> | |
| 69 | + | <tr> | |
| 70 | + | <td colspan="7"> | |
| 71 | + | <div id="error-message-{{.ID}}" class="text-red-500"></div> | |
| 72 | + | </td> | |
| 73 | + | </tr> | |
| 74 | + | {{ else }} | |
| 75 | + | <tr> | |
| 76 | + | <td colspan="7" class="mt-2">No Running containers.</td> | |
| 77 | + | </tr> | |
| 78 | + | {{ | |
| 79 | + | end | |
| 80 | + | }} | |
| 81 | + | </tbody> | |
| 82 | + | </table> | |
| 46 | 83 | </div> | |
| @@ -1,46 +1,83 @@ | |||||
| 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">Running containers</h1> | ||
| 3 | - | <div class="flex font-bold border-t-2 align-baseline py-2"> | 3 | + | <table class="p-4 border-t-2" hx-ext="response-targets"> |
| 4 | - | <div class="w-1/6">Name</div> | 4 | + | <!-- <thead> |
| 5 | - | <div class="w-1/6">Command</div> | 5 | + | <tr> |
| 6 | - | <div class="w-1/6">Created</div> | 6 | + | <th>Name</th> |
| 7 | - | <div class="w-1/6">Status</div> | 7 | + | <th>Command</th> |
| 8 | - | <div class="w-1/6">Port</div> | 8 | + | <th>Created</th> |
| 9 | - | <div class="w-1/6">State</div> | 9 | + | <th>Status</th> |
| 10 | - | <div class="w-1/4">Action</div> | 10 | + | <th>Port</th> |
| 11 | - | </div> | 11 | + | <th>State</th> |
| 12 | - | {{ range.Containers }} | 12 | + | <th>Action</th> |
| 13 | - | <div id="{{.ID}}" class="flex py-2 align-baseline justify-center"> | 13 | + | </tr> |
| 14 | - | <div class="w-1/6">{{ range.Names }}{{.}}<br />{{ end }}</div> | 14 | + | </thead> --> |
| 15 | - | <div class="w-1/6" style="text-decoration: underline">{{.Command}}</div> | 15 | + | <tbody> |
| 16 | - | <div class="w-1/6">{{.CreatedStr}}</div> | 16 | + | {{ |
| 17 | - | <div class="w-1/6">{{.Status}} (Up since {{.UpSince}})</div> | 17 | + | range.containers |
| 18 | - | <div class="w-1/6">{{ range.Ports }}{{.}}<br />{{ end }}</div> | 18 | + | }} |
| 19 | - | <div class="w-1/6" style="color: {{.StateColor}};">{{.State}}</div> | 19 | + | <tr id="container-{{.ID}}"> |
| 20 | - | <div class="w-1/4"> | 20 | + | <td> |
| 21 | - | <div x-data="{ success: false, loading: false }"> | 21 | + | <span title="{{.Container.ID}}">{{.Name}}</span> |
| 22 | - | <button | 22 | + | </td> |
| 23 | - | id="remove-button-{{.ShortID}}" | 23 | + | <td class="command-container" style="text-decoration: underline"> |
| 24 | - | x-bind:class="{ 'bg-green-500': success, 'bg-red-500': !success, 'text-white': true, 'px-2': true, 'py-1': true, 'rounded': true }" | 24 | + | <span title="{{.Command}}">{{.Command}}</span> |
| 25 | - | @click="loading = true" | 25 | + | </td> |
| 26 | - | hx-trigger="click" | 26 | + | <td>{{.CreatedStr}}</td> |
| 27 | - | hx-delete="/htmx/container-manager/delete/{{.ShortID}}" | 27 | + | <td class="port-container">{{ .Ports }}</td> |
| 28 | - | hx-swap="outerHTML swap:outerHTML" | 28 | + | <td class="status-container">{{.Status}} (Up since {{.UpSince}})</td> |
| 29 | - | hx-target-error="container-delete-images-error" | 29 | + | <td style="color: {{.StateColor}};">{{.State}}</td> |
| 30 | - | x-on:htmx:afterSwap="if ($event.detail.status === 200 && $event.detail.xhr.response === 'Success') { success = true; loading = false; setTimeout(() => location.reload(), 3000); }" | 30 | + | <td align="right"> |
| 31 | - | > | 31 | + | <!-- iconoir start --> |
| 32 | - | <span x-show="!loading && !success">Remove</span> | 32 | + | <button |
| 33 | - | <span x-show="success">Success</span> | 33 | + | id="start-button-{{.ID}}" |
| 34 | - | </button> | 34 | + | hx-post="/htmx/container-manager/start/{{.ID}}" |
| 35 | - | </div> | 35 | + | hx-target="#container-{{.ID}}" |
| 36 | - | </div> | 36 | + | hx-target-*="#error-message-{{.ID}}" |
| 37 | - | </div> | 37 | + | hx-swap="innerHTML" |
| 38 | - | {{ else }} | 38 | + | class="py-2 px-4" |
| 39 | - | <div class="mt-2">No Running containers.</div> | 39 | + | > |
| 40 | - | {{ end }} | 40 | + | <span class="iconoir-play text-xl font-bold"></span> |
| 41 | - | <div | 41 | + | </button> |
| 42 | - | id="container-delete-error" | 42 | + | <!-- stop --> |
| 43 | - | style="display: none" | 43 | + | <button |
| 44 | - | class="mt-2 text-red-800 text-lg" | 44 | + | id="stop-button-{{.ID}}" |
| 45 | - | ></div> | 45 | + | hx-post="/htmx/container-manager/stop/{{.ID}}" |
| 46 | + | hx-target="#container-{{.ID}}" | |||
| 47 | + | hx-target-*="#error-message-{{.ID}}" | |||
| 48 | + | hx-swap="innerHTML" | |||
| 49 | + | hx-confirm="Are you sure you want to stop this container?" | |||
| 50 | + | class="py-2 px-4" | |||
| 51 | + | > | |||
| 52 | + | <span class="iconoir-system-shut text-xl font-bold"></span> | |||
| 53 | + | </button> | |||
| 54 | + | <!-- REMOVE --> | |||
| 55 | + | <button | |||
| 56 | + | id="remove-button-img-{{.ID}}" | |||
| 57 | + | hx-delete="/htmx/container-manager/delete/{{.ID}}" | |||
| 58 | + | hx-target="#container-{{.ID}}" | |||
| 59 | + | hx-target-*="#error-message-{{.ID}}" | |||
| 60 | + | hx-swap="innerHTML" | |||
| 61 | + | hx-confirm="Are you sure you want to delete this container image?" | |||
| 62 | + | class="py-2 px-4" | |||
| 63 | + | > | |||
| 64 | + | <!-- iconoir trash --> | |||
| 65 | + | <span class="iconoir-trash text-xl text-red-600 font-bold"></span> | |||
| 66 | + | </button> | |||
| 67 | + | </td> | |||
| 68 | + | </tr> | |||
| 69 | + | <tr> | |||
| 70 | + | <td colspan="7"> | |||
| 71 | + | <div id="error-message-{{.ID}}" class="text-red-500"></div> | |||
| 72 | + | </td> | |||
| 73 | + | </tr> | |||
| 74 | + | {{ else }} | |||
| 75 | + | <tr> | |||
| 76 | + | <td colspan="7" class="mt-2">No Running containers.</td> | |||
| 77 | + | </tr> | |||
| 78 | + | {{ | |||
| 79 | + | end | |||
| 80 | + | }} | |||
| 81 | + | </tbody> | |||
| 82 | + | </table> | |||
| 46 | </div> | 83 | </div> | ||
M internal/gotemplate/models/html/fragments/imagelist.gohtml +50 -30
| @@ -1,32 +1,52 @@ | |||
| 1 | - | <div class="flex flex-col p-4" hx-ext="response-targets"> | |
| 1 | + | <div class="flex flex-col p-4"> | |
| 2 | 2 | <h1 class="text-lg md:text-xl font-bold">Images</h1> | |
| 3 | - | <div class="flex font-bold border-t-2 align-baseline py-2"> | |
| 4 | - | <div class="w-2/4">Name</div> | |
| 5 | - | <div class="w-2/4">Created</div> | |
| 6 | - | <div class="w-1/6">Size</div> | |
| 7 | - | <div class="w-1/4">Action</div> | |
| 8 | - | </div> | |
| 9 | - | {{ range.Images }} | |
| 10 | - | <div id="img-{{.ShortImgID}}" class="flex py-2 align-baseline justify-center"> | |
| 11 | - | <div class="w-2/4"> | |
| 12 | - | <span title="{{.ImageSummary.ID}}">{{.Name}}</span> | |
| 13 | - | </div> | |
| 14 | - | <div class="w-2/4">{{.CreatedStr}}</div> | |
| 15 | - | <div class="w-1/6">{{.SizeStr}}</div> | |
| 16 | - | <button | |
| 17 | - | id="remove-button-img-{{.ShortImgID}}" | |
| 18 | - | hx-delete="/htmx/image-manager/delete/{{.ShortImgID}}" | |
| 19 | - | hx-target="#img-{{.ShortImgID}}" | |
| 20 | - | hx-target-*="#error-message-img" | |
| 21 | - | hx-swap="innerHTML" | |
| 22 | - | hx-confirm="Are you sure you want to delete this container image?" | |
| 23 | - | class="w-1/4 bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-md" | |
| 24 | - | > | |
| 25 | - | Delete | |
| 26 | - | </button> | |
| 27 | - | </div> | |
| 28 | - | <div id="error-message-img" class="mt-2 text-red-500" display="none"></div> | |
| 29 | - | {{ else }} | |
| 30 | - | <div class="mt-2">No images found.</div> | |
| 31 | - | {{ end }} | |
| 3 | + | <table class="p-4 border-t-2"> | |
| 4 | + | <!-- <thead> | |
| 5 | + | <tr> | |
| 6 | + | <th>Name</th> | |
| 7 | + | <th>Created</th> | |
| 8 | + | <th>Size</th> | |
| 9 | + | <th>Action</th> | |
| 10 | + | </tr> | |
| 11 | + | </thead> --> | |
| 12 | + | <tbody> | |
| 13 | + | {{ | |
| 14 | + | range.Images | |
| 15 | + | }} | |
| 16 | + | <tr id="img-{{.ID}}" hx-ext="response-targets"> | |
| 17 | + | <td> | |
| 18 | + | <span title="{{.ImageSummary.ID}}">{{.Name}}</span> | |
| 19 | + | </td> | |
| 20 | + | <td> | |
| 21 | + | <span title="Image creation date">{{.CreatedStr}}</span> | |
| 22 | + | </td> | |
| 23 | + | <td>{{.SizeStr}}</td> | |
| 24 | + | <td align="right"> | |
| 25 | + | <button | |
| 26 | + | id="remove-button-img-{{.ID}}" | |
| 27 | + | hx-delete="/htmx/image-manager/delete/{{.ID}}" | |
| 28 | + | hx-target="#img-{{.ID}}" | |
| 29 | + | hx-target-*="#error-message-div-{{.ID}}" | |
| 30 | + | hx-swap="innerHTML" | |
| 31 | + | hx-confirm="Are you sure you want to delete this container image?" | |
| 32 | + | class="py-2 px-4" | |
| 33 | + | > | |
| 34 | + | <span class="iconoir-trash text-xl text-red-600 font-bold"></span> | |
| 35 | + | </button> | |
| 36 | + | </td> | |
| 37 | + | </tr> | |
| 38 | + | <t> | |
| 39 | + | <td colspan="4"> | |
| 40 | + | <div id="error-message-div-{{.ID}}" class="text-red-500"></div> | |
| 41 | + | </td> | |
| 42 | + | </tr> | |
| 43 | + | {{ else }} | |
| 44 | + | <tr> | |
| 45 | + | <td colspan="4" class="mt-2">No images found.</td> | |
| 46 | + | </tr> | |
| 47 | + | {{ | |
| 48 | + | end | |
| 49 | + | }} | |
| 50 | + | </tbody> | |
| 51 | + | </table> | |
| 32 | 52 | </div> | |
| @@ -1,32 +1,52 @@ | |||||
| 1 | - | <div class="flex flex-col p-4" hx-ext="response-targets"> | 1 | + | <div class="flex flex-col p-4"> |
| 2 | <h1 class="text-lg md:text-xl font-bold">Images</h1> | 2 | <h1 class="text-lg md:text-xl font-bold">Images</h1> | ||
| 3 | - | <div class="flex font-bold border-t-2 align-baseline py-2"> | 3 | + | <table class="p-4 border-t-2"> |
| 4 | - | <div class="w-2/4">Name</div> | 4 | + | <!-- <thead> |
| 5 | - | <div class="w-2/4">Created</div> | 5 | + | <tr> |
| 6 | - | <div class="w-1/6">Size</div> | 6 | + | <th>Name</th> |
| 7 | - | <div class="w-1/4">Action</div> | 7 | + | <th>Created</th> |
| 8 | - | </div> | 8 | + | <th>Size</th> |
| 9 | - | {{ range.Images }} | 9 | + | <th>Action</th> |
| 10 | - | <div id="img-{{.ShortImgID}}" class="flex py-2 align-baseline justify-center"> | 10 | + | </tr> |
| 11 | - | <div class="w-2/4"> | 11 | + | </thead> --> |
| 12 | - | <span title="{{.ImageSummary.ID}}">{{.Name}}</span> | 12 | + | <tbody> |
| 13 | - | </div> | 13 | + | {{ |
| 14 | - | <div class="w-2/4">{{.CreatedStr}}</div> | 14 | + | range.Images |
| 15 | - | <div class="w-1/6">{{.SizeStr}}</div> | 15 | + | }} |
| 16 | - | <button | 16 | + | <tr id="img-{{.ID}}" hx-ext="response-targets"> |
| 17 | - | id="remove-button-img-{{.ShortImgID}}" | 17 | + | <td> |
| 18 | - | hx-delete="/htmx/image-manager/delete/{{.ShortImgID}}" | 18 | + | <span title="{{.ImageSummary.ID}}">{{.Name}}</span> |
| 19 | - | hx-target="#img-{{.ShortImgID}}" | 19 | + | </td> |
| 20 | - | hx-target-*="#error-message-img" | 20 | + | <td> |
| 21 | - | hx-swap="innerHTML" | 21 | + | <span title="Image creation date">{{.CreatedStr}}</span> |
| 22 | - | hx-confirm="Are you sure you want to delete this container image?" | 22 | + | </td> |
| 23 | - | class="w-1/4 bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-md" | 23 | + | <td>{{.SizeStr}}</td> |
| 24 | - | > | 24 | + | <td align="right"> |
| 25 | - | Delete | 25 | + | <button |
| 26 | - | </button> | 26 | + | id="remove-button-img-{{.ID}}" |
| 27 | - | </div> | 27 | + | hx-delete="/htmx/image-manager/delete/{{.ID}}" |
| 28 | - | <div id="error-message-img" class="mt-2 text-red-500" display="none"></div> | 28 | + | hx-target="#img-{{.ID}}" |
| 29 | - | {{ else }} | 29 | + | hx-target-*="#error-message-div-{{.ID}}" |
| 30 | - | <div class="mt-2">No images found.</div> | 30 | + | hx-swap="innerHTML" |
| 31 | - | {{ end }} | 31 | + | hx-confirm="Are you sure you want to delete this container image?" |
| 32 | + | class="py-2 px-4" | |||
| 33 | + | > | |||
| 34 | + | <span class="iconoir-trash text-xl text-red-600 font-bold"></span> | |||
| 35 | + | </button> | |||
| 36 | + | </td> | |||
| 37 | + | </tr> | |||
| 38 | + | <t> | |||
| 39 | + | <td colspan="4"> | |||
| 40 | + | <div id="error-message-div-{{.ID}}" class="text-red-500"></div> | |||
| 41 | + | </td> | |||
| 42 | + | </tr> | |||
| 43 | + | {{ else }} | |||
| 44 | + | <tr> | |||
| 45 | + | <td colspan="4" class="mt-2">No images found.</td> | |||
| 46 | + | </tr> | |||
| 47 | + | {{ | |||
| 48 | + | end | |||
| 49 | + | }} | |||
| 50 | + | </tbody> | |||
| 51 | + | </table> | |||
| 32 | </div> | 52 | </div> | ||
M internal/httpserve/handler/docker.go +88 -45
| @@ -3,29 +3,33 @@ package handler | |||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | 5 | "net/http" | |
| 6 | + | "sync" | |
| 6 | 7 | "time" | |
| 7 | 8 | ||
| 8 | 9 | "github.com/bnema/gordon/internal/app" | |
| 9 | 10 | "github.com/bnema/gordon/internal/gotemplate/render" | |
| 10 | 11 | "github.com/bnema/gordon/pkg/utils/docker" | |
| 11 | 12 | "github.com/bnema/gordon/pkg/utils/humanize" | |
| 12 | - | "github.com/bnema/gordon/pkg/utils/sanitize" | |
| 13 | 13 | "github.com/docker/docker/api/types" | |
| 14 | 14 | "github.com/labstack/echo/v4" | |
| 15 | 15 | ) | |
| 16 | 16 | ||
| 17 | 17 | var idMap = make(map[string]string) | |
| 18 | + | var idMapMutex sync.Mutex | |
| 18 | 19 | ||
| 19 | 20 | type HumanReadableContainerImage struct { | |
| 20 | 21 | *types.ImageSummary | |
| 21 | 22 | Name string | |
| 22 | - | ShortImgID string | |
| 23 | + | ShortID string | |
| 23 | 24 | CreatedStr string | |
| 24 | 25 | SizeStr string | |
| 25 | 26 | } | |
| 26 | 27 | ||
| 27 | 28 | type HumanReadableContainer struct { | |
| 28 | 29 | *types.Container | |
| 30 | + | Name string | |
| 31 | + | Ports []string | |
| 32 | + | ShortID string | |
| 29 | 33 | CreatedStr string // Human-readable Created time | |
| 30 | 34 | SizeStr string // Human-readable Size | |
| 31 | 35 | UpSince string // Human-readable time elapsed since the container was started | |
| @@ -36,29 +40,26 @@ type HumanReadableContainer struct { | |||
| 36 | 40 | func ImageManagerHandler(c echo.Context, a *app.App) error { | |
| 37 | 41 | images, err := docker.ListContainerImages() | |
| 38 | 42 | if err != nil { | |
| 39 | - | rawErrHTML := `<div>Error: ` + err.Error() + `</div>` | |
| 40 | - | sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML) | |
| 41 | - | if err != nil { | |
| 42 | - | return c.String(http.StatusInternalServerError, "An error occurred during sanitization") | |
| 43 | - | } | |
| 44 | - | return c.HTML(http.StatusInternalServerError, sanitizedHTML) | |
| 43 | + | return sendError(c, err) | |
| 45 | 44 | } | |
| 46 | - | ||
| 47 | 45 | var humanReadableImages []HumanReadableContainerImage | |
| 48 | 46 | ||
| 49 | 47 | for _, image := range images { | |
| 50 | - | ShortImgID := image.ID[7:19] | |
| 51 | - | idMap[ShortImgID] = image.ID | |
| 48 | + | ShortID := image.ID[7:19] | |
| 49 | + | idMapMutex.Lock() | |
| 50 | + | idMap[ShortID] = image.ID | |
| 51 | + | idMapMutex.Unlock() | |
| 52 | 52 | createdTime := time.Unix(image.Created, 0) | |
| 53 | 53 | createdStr := humanize.TimeAgo(createdTime) | |
| 54 | 54 | sizeStr := humanize.BytesToReadableSize(image.Size) | |
| 55 | 55 | for _, repoTag := range image.RepoTags { | |
| 56 | 56 | humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{ | |
| 57 | - | ShortImgID: ShortImgID, | |
| 58 | - | ImageSummary: &image, | |
| 59 | - | CreatedStr: createdStr, | |
| 60 | - | SizeStr: sizeStr, | |
| 61 | - | Name: repoTag, | |
| 57 | + | ImageSummary: &types.ImageSummary{ | |
| 58 | + | ID: ShortID, // Set the ID to the ShortID | |
| 59 | + | }, | |
| 60 | + | CreatedStr: createdStr, | |
| 61 | + | SizeStr: sizeStr, | |
| 62 | + | Name: repoTag, | |
| 62 | 63 | }) | |
| 63 | 64 | } | |
| 64 | 65 | } | |
| @@ -80,22 +81,29 @@ func ImageManagerHandler(c echo.Context, a *app.App) error { | |||
| 80 | 81 | // ImageManagerDeleteHandler handles the /image-manager/delete route | |
| 81 | 82 | func ImageManagerDeleteHandler(c echo.Context, a *app.App) error { | |
| 82 | 83 | // Get the ShortImgID from the URL | |
| 83 | - | ShortImgID := c.Param("ShortImgID") | |
| 84 | - | fmt.Println("ShortImgID:", ShortImgID) | |
| 84 | + | ShortID := c.Param("ID") | |
| 85 | + | ||
| 86 | + | idMapMutex.Lock() | |
| 85 | 87 | // Check if the ShortImgID exists in the idMap | |
| 86 | - | imageID, exists := idMap[ShortImgID] | |
| 87 | - | fmt.Println("ImageID:", imageID) | |
| 88 | + | imageID, exists := idMap[ShortID] | |
| 89 | + | idMapMutex.Unlock() | |
| 90 | + | ||
| 88 | 91 | if !exists { | |
| 89 | 92 | return c.String(http.StatusBadRequest, "Invalid ShortImgID") | |
| 90 | 93 | } | |
| 94 | + | ||
| 91 | 95 | err := docker.DeleteContainerImage(imageID) | |
| 92 | 96 | if err != nil { | |
| 93 | 97 | c.Response().Header().Set("X-Error-Type", "image") | |
| 94 | 98 | return c.String(http.StatusInternalServerError, err.Error()) | |
| 95 | - | ||
| 96 | 99 | } | |
| 97 | 100 | ||
| 98 | - | // Since it is HTMX we return a html div with the message "Removed" | |
| 101 | + | // Remove the entry from idMap | |
| 102 | + | idMapMutex.Lock() | |
| 103 | + | delete(idMap, ShortID) | |
| 104 | + | idMapMutex.Unlock() | |
| 105 | + | ||
| 106 | + | // Since it is HTMX we return a HTML div with the message "Removed" | |
| 99 | 107 | return c.HTML(http.StatusOK, `<div>Removed</div>`) | |
| 100 | 108 | } | |
| 101 | 109 | ||
| @@ -103,16 +111,44 @@ func ImageManagerDeleteHandler(c echo.Context, a *app.App) error { | |||
| 103 | 111 | func ContainerManagerHandler(c echo.Context, a *app.App) error { | |
| 104 | 112 | containers, err := docker.ListRunningContainers() | |
| 105 | 113 | if err != nil { | |
| 106 | - | rawErrHTML := `<div>Error: ` + err.Error() + `</div>` | |
| 107 | - | sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML) | |
| 108 | - | if err != nil { | |
| 109 | - | return c.String(http.StatusInternalServerError, "An error occurred during sanitization") | |
| 110 | - | } | |
| 111 | - | return c.HTML(http.StatusInternalServerError, sanitizedHTML) | |
| 114 | + | return sendError(c, err) | |
| 112 | 115 | } | |
| 116 | + | // We need Name / Command / Created / Status / Ports / State | |
| 117 | + | var humanReadableContainers []HumanReadableContainer | |
| 118 | + | ||
| 119 | + | for _, container := range containers { | |
| 120 | + | ShortID := container.ID[0:12] | |
| 121 | + | idMap[ShortID] = container.ID | |
| 122 | + | createdTime := time.Unix(container.Created, 0) | |
| 123 | + | createdStr := humanize.TimeAgo(createdTime) | |
| 124 | + | sizeStr := humanize.BytesToReadableSize(container.SizeRw) | |
| 125 | + | stateColor := "green" | |
| 126 | + | // If the container is exited, set the state color to red | |
| 127 | + | if container.State == "exited" { | |
| 128 | + | stateColor = "red" | |
| 129 | + | } | |
| 130 | + | var ports []string | |
| 131 | + | for _, port := range container.Ports { | |
| 132 | + | portStr := fmt.Sprintf("%d:%d", port.PublicPort, port.PrivatePort) | |
| 133 | + | ports = append(ports, portStr) | |
| 134 | + | } | |
| 113 | 135 | ||
| 136 | + | for _, name := range container.Names { | |
| 137 | + | name = name[1:] | |
| 138 | + | humanReadableContainers = append(humanReadableContainers, HumanReadableContainer{ | |
| 139 | + | Container: &container, | |
| 140 | + | CreatedStr: createdStr, | |
| 141 | + | SizeStr: sizeStr, | |
| 142 | + | UpSince: humanize.TimeAgo(time.Unix(container.Created, 0)), | |
| 143 | + | StateColor: stateColor, | |
| 144 | + | Name: name, | |
| 145 | + | ShortID: ShortID, | |
| 146 | + | Ports: ports, | |
| 147 | + | }) | |
| 148 | + | } | |
| 149 | + | } | |
| 114 | 150 | data := map[string]interface{}{ | |
| 115 | - | "Containers": containers, | |
| 151 | + | "containers": humanReadableContainers, | |
| 116 | 152 | } | |
| 117 | 153 | ||
| 118 | 154 | rendererData, err := render.GetHTMLRenderer("html/fragments", "containerlist.gohtml", a.TemplateFS, a) | |
| @@ -128,25 +164,32 @@ func ContainerManagerHandler(c echo.Context, a *app.App) error { | |||
| 128 | 164 | ||
| 129 | 165 | // ContainerManagerDeleteHandler handles the /container-manager/delete route | |
| 130 | 166 | func ContainerManagerDeleteHandler(c echo.Context, a *app.App) error { | |
| 131 | - | // Get the ShortID from the URL | |
| 132 | - | ShortID := c.Param("ShortID") | |
| 133 | - | // Check if the shortID exists in the idMap | |
| 134 | - | containerID, exists := idMap[ShortID] | |
| 135 | - | if !exists { | |
| 136 | - | return c.String(http.StatusBadRequest, "Invalid shortID") | |
| 167 | + | ID := c.Param("ID") | |
| 168 | + | err := docker.DeleteContainer(ID) | |
| 169 | + | if err != nil { | |
| 170 | + | return sendError(c, err) | |
| 137 | 171 | } | |
| 138 | - | fmt.Println("ShortID:", ShortID, "ContainerID:", containerID) | |
| 172 | + | // Since it is HTMX we return a html div with the message "Removed" | |
| 173 | + | return c.HTML(http.StatusOK, `Success`) | |
| 174 | + | } | |
| 139 | 175 | ||
| 140 | - | err := docker.DeleteContainer(containerID) | |
| 176 | + | // ContainerManagerStopHandler handles the /container-manager/stop route | |
| 177 | + | func ContainerManagerStopHandler(c echo.Context, a *app.App) error { | |
| 178 | + | ID := c.Param("ID") | |
| 179 | + | // Stop the container gracefully with a timeout of 10 seconds | |
| 180 | + | stopped, err := docker.StopContainerGracefully(ID, 3*time.Second) | |
| 141 | 181 | if err != nil { | |
| 142 | - | rawErrHTML := `<div>Error: ` + err.Error() + `</div>` | |
| 143 | - | sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML) | |
| 144 | - | if err != nil { | |
| 145 | - | // Handle the sanitization error | |
| 146 | - | return c.String(http.StatusInternalServerError, "An error occurred during sanitization") | |
| 147 | - | } | |
| 148 | - | return c.HTML(http.StatusInternalServerError, sanitizedHTML) | |
| 182 | + | return sendError(c, err) | |
| 149 | 183 | } | |
| 150 | - | // Since it is HTMX we return a html div with the message "Removed" | |
| 184 | + | // if stopped is true, the container was stopped gracefully | |
| 185 | + | if stopped { | |
| 186 | + | return c.HTML(http.StatusOK, `Success`) | |
| 187 | + | } | |
| 188 | + | // If stopped is false, the container was not stopped gracefully we force stop it | |
| 189 | + | err = docker.StopContainerRagefully(ID) | |
| 190 | + | if err != nil { | |
| 191 | + | return sendError(c, err) | |
| 192 | + | } | |
| 193 | + | ||
| 151 | 194 | return c.HTML(http.StatusOK, `Success`) | |
| 152 | 195 | } | |
| @@ -3,29 +3,33 @@ package handler | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | "net/http" | 5 | "net/http" | ||
| 6 | + | "sync" | |||
| 6 | "time" | 7 | "time" | ||
| 7 | 8 | ||||
| 8 | "github.com/bnema/gordon/internal/app" | 9 | "github.com/bnema/gordon/internal/app" | ||
| 9 | "github.com/bnema/gordon/internal/gotemplate/render" | 10 | "github.com/bnema/gordon/internal/gotemplate/render" | ||
| 10 | "github.com/bnema/gordon/pkg/utils/docker" | 11 | "github.com/bnema/gordon/pkg/utils/docker" | ||
| 11 | "github.com/bnema/gordon/pkg/utils/humanize" | 12 | "github.com/bnema/gordon/pkg/utils/humanize" | ||
| 12 | - | "github.com/bnema/gordon/pkg/utils/sanitize" | |||
| 13 | "github.com/docker/docker/api/types" | 13 | "github.com/docker/docker/api/types" | ||
| 14 | "github.com/labstack/echo/v4" | 14 | "github.com/labstack/echo/v4" | ||
| 15 | ) | 15 | ) | ||
| 16 | 16 | ||||
| 17 | var idMap = make(map[string]string) | 17 | var idMap = make(map[string]string) | ||
| 18 | + | var idMapMutex sync.Mutex | |||
| 18 | 19 | ||||
| 19 | type HumanReadableContainerImage struct { | 20 | type HumanReadableContainerImage struct { | ||
| 20 | *types.ImageSummary | 21 | *types.ImageSummary | ||
| 21 | Name string | 22 | Name string | ||
| 22 | - | ShortImgID string | 23 | + | ShortID string |
| 23 | CreatedStr string | 24 | CreatedStr string | ||
| 24 | SizeStr string | 25 | SizeStr string | ||
| 25 | } | 26 | } | ||
| 26 | 27 | ||||
| 27 | type HumanReadableContainer struct { | 28 | type HumanReadableContainer struct { | ||
| 28 | *types.Container | 29 | *types.Container | ||
| 30 | + | Name string | |||
| 31 | + | Ports []string | |||
| 32 | + | ShortID string | |||
| 29 | CreatedStr string // Human-readable Created time | 33 | CreatedStr string // Human-readable Created time | ||
| 30 | SizeStr string // Human-readable Size | 34 | SizeStr string // Human-readable Size | ||
| 31 | UpSince string // Human-readable time elapsed since the container was started | 35 | UpSince string // Human-readable time elapsed since the container was started | ||
| @@ -36,29 +40,26 @@ type HumanReadableContainer struct { | |||||
| 36 | func ImageManagerHandler(c echo.Context, a *app.App) error { | 40 | func ImageManagerHandler(c echo.Context, a *app.App) error { | ||
| 37 | images, err := docker.ListContainerImages() | 41 | images, err := docker.ListContainerImages() | ||
| 38 | if err != nil { | 42 | if err != nil { | ||
| 39 | - | rawErrHTML := `<div>Error: ` + err.Error() + `</div>` | 43 | + | return sendError(c, err) |
| 40 | - | sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML) | |||
| 41 | - | if err != nil { | |||
| 42 | - | return c.String(http.StatusInternalServerError, "An error occurred during sanitization") | |||
| 43 | - | } | |||
| 44 | - | return c.HTML(http.StatusInternalServerError, sanitizedHTML) | |||
| 45 | } | 44 | } | ||
| 46 | - | ||||
| 47 | var humanReadableImages []HumanReadableContainerImage | 45 | var humanReadableImages []HumanReadableContainerImage | ||
| 48 | 46 | ||||
| 49 | for _, image := range images { | 47 | for _, image := range images { | ||
| 50 | - | ShortImgID := image.ID[7:19] | 48 | + | ShortID := image.ID[7:19] |
| 51 | - | idMap[ShortImgID] = image.ID | 49 | + | idMapMutex.Lock() |
| 50 | + | idMap[ShortID] = image.ID | |||
| 51 | + | idMapMutex.Unlock() | |||
| 52 | createdTime := time.Unix(image.Created, 0) | 52 | createdTime := time.Unix(image.Created, 0) | ||
| 53 | createdStr := humanize.TimeAgo(createdTime) | 53 | createdStr := humanize.TimeAgo(createdTime) | ||
| 54 | sizeStr := humanize.BytesToReadableSize(image.Size) | 54 | sizeStr := humanize.BytesToReadableSize(image.Size) | ||
| 55 | for _, repoTag := range image.RepoTags { | 55 | for _, repoTag := range image.RepoTags { | ||
| 56 | humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{ | 56 | humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{ | ||
| 57 | - | ShortImgID: ShortImgID, | 57 | + | ImageSummary: &types.ImageSummary{ |
| 58 | - | ImageSummary: &image, | 58 | + | ID: ShortID, // Set the ID to the ShortID |
| 59 | - | CreatedStr: createdStr, | 59 | + | }, |
| 60 | - | SizeStr: sizeStr, | 60 | + | CreatedStr: createdStr, |
| 61 | - | Name: repoTag, | 61 | + | SizeStr: sizeStr, |
| 62 | + | Name: repoTag, | |||
| 62 | }) | 63 | }) | ||
| 63 | } | 64 | } | ||
| 64 | } | 65 | } | ||
| @@ -80,22 +81,29 @@ func ImageManagerHandler(c echo.Context, a *app.App) error { | |||||
| 80 | // ImageManagerDeleteHandler handles the /image-manager/delete route | 81 | // ImageManagerDeleteHandler handles the /image-manager/delete route | ||
| 81 | func ImageManagerDeleteHandler(c echo.Context, a *app.App) error { | 82 | func ImageManagerDeleteHandler(c echo.Context, a *app.App) error { | ||
| 82 | // Get the ShortImgID from the URL | 83 | // Get the ShortImgID from the URL | ||
| 83 | - | ShortImgID := c.Param("ShortImgID") | 84 | + | ShortID := c.Param("ID") |
| 84 | - | fmt.Println("ShortImgID:", ShortImgID) | 85 | + | |
| 86 | + | idMapMutex.Lock() | |||
| 85 | // Check if the ShortImgID exists in the idMap | 87 | // Check if the ShortImgID exists in the idMap | ||
| 86 | - | imageID, exists := idMap[ShortImgID] | 88 | + | imageID, exists := idMap[ShortID] |
| 87 | - | fmt.Println("ImageID:", imageID) | 89 | + | idMapMutex.Unlock() |
| 90 | + | ||||
| 88 | if !exists { | 91 | if !exists { | ||
| 89 | return c.String(http.StatusBadRequest, "Invalid ShortImgID") | 92 | return c.String(http.StatusBadRequest, "Invalid ShortImgID") | ||
| 90 | } | 93 | } | ||
| 94 | + | ||||
| 91 | err := docker.DeleteContainerImage(imageID) | 95 | err := docker.DeleteContainerImage(imageID) | ||
| 92 | if err != nil { | 96 | if err != nil { | ||
| 93 | c.Response().Header().Set("X-Error-Type", "image") | 97 | c.Response().Header().Set("X-Error-Type", "image") | ||
| 94 | return c.String(http.StatusInternalServerError, err.Error()) | 98 | return c.String(http.StatusInternalServerError, err.Error()) | ||
| 95 | - | ||||
| 96 | } | 99 | } | ||
| 97 | 100 | ||||
| 98 | - | // Since it is HTMX we return a html div with the message "Removed" | 101 | + | // Remove the entry from idMap |
| 102 | + | idMapMutex.Lock() | |||
| 103 | + | delete(idMap, ShortID) | |||
| 104 | + | idMapMutex.Unlock() | |||
| 105 | + | ||||
| 106 | + | // Since it is HTMX we return a HTML div with the message "Removed" | |||
| 99 | return c.HTML(http.StatusOK, `<div>Removed</div>`) | 107 | return c.HTML(http.StatusOK, `<div>Removed</div>`) | ||
| 100 | } | 108 | } | ||
| 101 | 109 | ||||
| @@ -103,16 +111,44 @@ func ImageManagerDeleteHandler(c echo.Context, a *app.App) error { | |||||
| 103 | func ContainerManagerHandler(c echo.Context, a *app.App) error { | 111 | func ContainerManagerHandler(c echo.Context, a *app.App) error { | ||
| 104 | containers, err := docker.ListRunningContainers() | 112 | containers, err := docker.ListRunningContainers() | ||
| 105 | if err != nil { | 113 | if err != nil { | ||
| 106 | - | rawErrHTML := `<div>Error: ` + err.Error() + `</div>` | 114 | + | return sendError(c, err) |
| 107 | - | sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML) | |||
| 108 | - | if err != nil { | |||
| 109 | - | return c.String(http.StatusInternalServerError, "An error occurred during sanitization") | |||
| 110 | - | } | |||
| 111 | - | return c.HTML(http.StatusInternalServerError, sanitizedHTML) | |||
| 112 | } | 115 | } | ||
| 116 | + | // We need Name / Command / Created / Status / Ports / State | |||
| 117 | + | var humanReadableContainers []HumanReadableContainer | |||
| 118 | + | ||||
| 119 | + | for _, container := range containers { | |||
| 120 | + | ShortID := container.ID[0:12] | |||
| 121 | + | idMap[ShortID] = container.ID | |||
| 122 | + | createdTime := time.Unix(container.Created, 0) | |||
| 123 | + | createdStr := humanize.TimeAgo(createdTime) | |||
| 124 | + | sizeStr := humanize.BytesToReadableSize(container.SizeRw) | |||
| 125 | + | stateColor := "green" | |||
| 126 | + | // If the container is exited, set the state color to red | |||
| 127 | + | if container.State == "exited" { | |||
| 128 | + | stateColor = "red" | |||
| 129 | + | } | |||
| 130 | + | var ports []string | |||
| 131 | + | for _, port := range container.Ports { | |||
| 132 | + | portStr := fmt.Sprintf("%d:%d", port.PublicPort, port.PrivatePort) | |||
| 133 | + | ports = append(ports, portStr) | |||
| 134 | + | } | |||
| 113 | 135 | ||||
| 136 | + | for _, name := range container.Names { | |||
| 137 | + | name = name[1:] | |||
| 138 | + | humanReadableContainers = append(humanReadableContainers, HumanReadableContainer{ | |||
| 139 | + | Container: &container, | |||
| 140 | + | CreatedStr: createdStr, | |||
| 141 | + | SizeStr: sizeStr, | |||
| 142 | + | UpSince: humanize.TimeAgo(time.Unix(container.Created, 0)), | |||
| 143 | + | StateColor: stateColor, | |||
| 144 | + | Name: name, | |||
| 145 | + | ShortID: ShortID, | |||
| 146 | + | Ports: ports, | |||
| 147 | + | }) | |||
| 148 | + | } | |||
| 149 | + | } | |||
| 114 | data := map[string]interface{}{ | 150 | data := map[string]interface{}{ | ||
| 115 | - | "Containers": containers, | 151 | + | "containers": humanReadableContainers, |
| 116 | } | 152 | } | ||
| 117 | 153 | ||||
| 118 | rendererData, err := render.GetHTMLRenderer("html/fragments", "containerlist.gohtml", a.TemplateFS, a) | 154 | rendererData, err := render.GetHTMLRenderer("html/fragments", "containerlist.gohtml", a.TemplateFS, a) | ||
| @@ -128,25 +164,32 @@ func ContainerManagerHandler(c echo.Context, a *app.App) error { | |||||
| 128 | 164 | ||||
| 129 | // ContainerManagerDeleteHandler handles the /container-manager/delete route | 165 | // ContainerManagerDeleteHandler handles the /container-manager/delete route | ||
| 130 | func ContainerManagerDeleteHandler(c echo.Context, a *app.App) error { | 166 | func ContainerManagerDeleteHandler(c echo.Context, a *app.App) error { | ||
| 131 | - | // Get the ShortID from the URL | 167 | + | ID := c.Param("ID") |
| 132 | - | ShortID := c.Param("ShortID") | 168 | + | err := docker.DeleteContainer(ID) |
| 133 | - | // Check if the shortID exists in the idMap | 169 | + | if err != nil { |
| 134 | - | containerID, exists := idMap[ShortID] | 170 | + | return sendError(c, err) |
| 135 | - | if !exists { | |||
| 136 | - | return c.String(http.StatusBadRequest, "Invalid shortID") | |||
| 137 | } | 171 | } | ||
| 138 | - | fmt.Println("ShortID:", ShortID, "ContainerID:", containerID) | 172 | + | // Since it is HTMX we return a html div with the message "Removed" |
| 173 | + | return c.HTML(http.StatusOK, `Success`) | |||
| 174 | + | } | |||
| 139 | 175 | ||||
| 140 | - | err := docker.DeleteContainer(containerID) | 176 | + | // ContainerManagerStopHandler handles the /container-manager/stop route |
| 177 | + | func ContainerManagerStopHandler(c echo.Context, a *app.App) error { | |||
| 178 | + | ID := c.Param("ID") | |||
| 179 | + | // Stop the container gracefully with a timeout of 10 seconds | |||
| 180 | + | stopped, err := docker.StopContainerGracefully(ID, 3*time.Second) | |||
| 141 | if err != nil { | 181 | if err != nil { | ||
| 142 | - | rawErrHTML := `<div>Error: ` + err.Error() + `</div>` | 182 | + | return sendError(c, err) |
| 143 | - | sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML) | |||
| 144 | - | if err != nil { | |||
| 145 | - | // Handle the sanitization error | |||
| 146 | - | return c.String(http.StatusInternalServerError, "An error occurred during sanitization") | |||
| 147 | - | } | |||
| 148 | - | return c.HTML(http.StatusInternalServerError, sanitizedHTML) | |||
| 149 | } | 183 | } | ||
| 150 | - | // Since it is HTMX we return a html div with the message "Removed" | 184 | + | // if stopped is true, the container was stopped gracefully |
| 185 | + | if stopped { | |||
| 186 | + | return c.HTML(http.StatusOK, `Success`) | |||
| 187 | + | } | |||
| 188 | + | // If stopped is false, the container was not stopped gracefully we force stop it | |||
| 189 | + | err = docker.StopContainerRagefully(ID) | |||
| 190 | + | if err != nil { | |||
| 191 | + | return sendError(c, err) | |||
| 192 | + | } | |||
| 193 | + | ||||
| 151 | return c.HTML(http.StatusOK, `Success`) | 194 | return c.HTML(http.StatusOK, `Success`) | ||
| 152 | } | 195 | } | ||
A internal/httpserve/handler/senderror.go +17 -0
| @@ -0,0 +1,17 @@ | |||
| 1 | + | package handler | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "net/http" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/gordon/pkg/utils/sanitize" | |
| 7 | + | "github.com/labstack/echo/v4" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func sendError(c echo.Context, err error) error { | |
| 11 | + | rawErrHTML := `<div>Error: ` + err.Error() + `</div>` | |
| 12 | + | sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML) | |
| 13 | + | if err != nil { | |
| 14 | + | return c.String(http.StatusInternalServerError, "An error occurred during sanitization") | |
| 15 | + | } | |
| 16 | + | return c.HTML(http.StatusInternalServerError, sanitizedHTML) | |
| 17 | + | } | |
| @@ -0,0 +1,17 @@ | |||||
| 1 | + | package handler | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "net/http" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/gordon/pkg/utils/sanitize" | |||
| 7 | + | "github.com/labstack/echo/v4" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func sendError(c echo.Context, err error) error { | |||
| 11 | + | rawErrHTML := `<div>Error: ` + err.Error() + `</div>` | |||
| 12 | + | sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML) | |||
| 13 | + | if err != nil { | |||
| 14 | + | return c.String(http.StatusInternalServerError, "An error occurred during sanitization") | |||
| 15 | + | } | |||
| 16 | + | return c.HTML(http.StatusInternalServerError, sanitizedHTML) | |||
| 17 | + | } | |||
M internal/httpserve/router.go +6 -2
| @@ -70,14 +70,18 @@ func bindHTMXEndpoints(e *echo.Echo, a *app.App) { | |||
| 70 | 70 | e.GET("/htmx/image-manager", func(c echo.Context) error { | |
| 71 | 71 | return handler.ImageManagerHandler(c, a) | |
| 72 | 72 | }) | |
| 73 | - | e.DELETE("/htmx/image-manager/delete/:ShortImgID", func(c echo.Context) error { | |
| 73 | + | e.DELETE("/htmx/image-manager/delete/:ID", func(c echo.Context) error { | |
| 74 | 74 | return handler.ImageManagerDeleteHandler(c, a) | |
| 75 | 75 | }) | |
| 76 | 76 | ||
| 77 | 77 | e.GET("/htmx/container-manager", func(c echo.Context) error { | |
| 78 | 78 | return handler.ContainerManagerHandler(c, a) | |
| 79 | 79 | }) | |
| 80 | - | e.DELETE("/htmx/container-manager/delete/:ShortID", func(c echo.Context) error { | |
| 80 | + | e.POST("/htmx/container-manager/stop/:ID", func(c echo.Context) error { | |
| 81 | + | return handler.ContainerManagerStopHandler(c, a) | |
| 82 | + | }) | |
| 83 | + | ||
| 84 | + | e.DELETE("/htmx/container-manager/delete/:ID", func(c echo.Context) error { | |
| 81 | 85 | return handler.ContainerManagerDeleteHandler(c, a) | |
| 82 | 86 | }) | |
| 83 | 87 | } | |
| @@ -70,14 +70,18 @@ func bindHTMXEndpoints(e *echo.Echo, a *app.App) { | |||||
| 70 | e.GET("/htmx/image-manager", func(c echo.Context) error { | 70 | e.GET("/htmx/image-manager", func(c echo.Context) error { | ||
| 71 | return handler.ImageManagerHandler(c, a) | 71 | return handler.ImageManagerHandler(c, a) | ||
| 72 | }) | 72 | }) | ||
| 73 | - | e.DELETE("/htmx/image-manager/delete/:ShortImgID", func(c echo.Context) error { | 73 | + | e.DELETE("/htmx/image-manager/delete/:ID", func(c echo.Context) error { |
| 74 | return handler.ImageManagerDeleteHandler(c, a) | 74 | return handler.ImageManagerDeleteHandler(c, a) | ||
| 75 | }) | 75 | }) | ||
| 76 | 76 | ||||
| 77 | e.GET("/htmx/container-manager", func(c echo.Context) error { | 77 | e.GET("/htmx/container-manager", func(c echo.Context) error { | ||
| 78 | return handler.ContainerManagerHandler(c, a) | 78 | return handler.ContainerManagerHandler(c, a) | ||
| 79 | }) | 79 | }) | ||
| 80 | - | e.DELETE("/htmx/container-manager/delete/:ShortID", func(c echo.Context) error { | 80 | + | e.POST("/htmx/container-manager/stop/:ID", func(c echo.Context) error { |
| 81 | + | return handler.ContainerManagerStopHandler(c, a) | |||
| 82 | + | }) | |||
| 83 | + | ||||
| 84 | + | e.DELETE("/htmx/container-manager/delete/:ID", func(c echo.Context) error { | |||
| 81 | return handler.ContainerManagerDeleteHandler(c, a) | 85 | return handler.ContainerManagerDeleteHandler(c, a) | ||
| 82 | }) | 86 | }) | ||
| 83 | } | 87 | } | ||
M internal/webui/public/assets/css/tailwind.css +79 -46
| @@ -575,6 +575,10 @@ video { | |||
| 575 | 575 | } | |
| 576 | 576 | } | |
| 577 | 577 | ||
| 578 | + | .relative { | |
| 579 | + | position: relative; | |
| 580 | + | } | |
| 581 | + | ||
| 578 | 582 | .my-2 { | |
| 579 | 583 | margin-top: 0.5rem; | |
| 580 | 584 | margin-bottom: 0.5rem; | |
| @@ -592,18 +596,22 @@ video { | |||
| 592 | 596 | margin-right: 0.5rem; | |
| 593 | 597 | } | |
| 594 | 598 | ||
| 595 | - | .mt-2 { | |
| 596 | - | margin-top: 0.5rem; | |
| 597 | - | } | |
| 598 | - | ||
| 599 | 599 | .mt-4 { | |
| 600 | 600 | margin-top: 1rem; | |
| 601 | 601 | } | |
| 602 | 602 | ||
| 603 | + | .mt-2 { | |
| 604 | + | margin-top: 0.5rem; | |
| 605 | + | } | |
| 606 | + | ||
| 603 | 607 | .flex { | |
| 604 | 608 | display: flex; | |
| 605 | 609 | } | |
| 606 | 610 | ||
| 611 | + | .table { | |
| 612 | + | display: table; | |
| 613 | + | } | |
| 614 | + | ||
| 607 | 615 | .hidden { | |
| 608 | 616 | display: none; | |
| 609 | 617 | } | |
| @@ -636,22 +644,10 @@ video { | |||
| 636 | 644 | width: 50%; | |
| 637 | 645 | } | |
| 638 | 646 | ||
| 639 | - | .w-1\/4 { | |
| 640 | - | width: 25%; | |
| 641 | - | } | |
| 642 | - | ||
| 643 | - | .w-1\/6 { | |
| 644 | - | width: 16.666667%; | |
| 645 | - | } | |
| 646 | - | ||
| 647 | 647 | .w-2\/3 { | |
| 648 | 648 | width: 66.666667%; | |
| 649 | 649 | } | |
| 650 | 650 | ||
| 651 | - | .w-2\/4 { | |
| 652 | - | width: 50%; | |
| 653 | - | } | |
| 654 | - | ||
| 655 | 651 | .w-72 { | |
| 656 | 652 | width: 18rem; | |
| 657 | 653 | } | |
| @@ -660,6 +656,18 @@ video { | |||
| 660 | 656 | width: 100%; | |
| 661 | 657 | } | |
| 662 | 658 | ||
| 659 | + | .w-1\/4 { | |
| 660 | + | width: 25%; | |
| 661 | + | } | |
| 662 | + | ||
| 663 | + | .w-1\/6 { | |
| 664 | + | width: 16.666667%; | |
| 665 | + | } | |
| 666 | + | ||
| 667 | + | .w-2\/4 { | |
| 668 | + | width: 50%; | |
| 669 | + | } | |
| 670 | + | ||
| 663 | 671 | .min-w-fit { | |
| 664 | 672 | min-width: -moz-fit-content; | |
| 665 | 673 | min-width: fit-content; | |
| @@ -717,10 +725,6 @@ video { | |||
| 717 | 725 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); | |
| 718 | 726 | } | |
| 719 | 727 | ||
| 720 | - | .rounded { | |
| 721 | - | border-radius: 0.25rem; | |
| 722 | - | } | |
| 723 | - | ||
| 724 | 728 | .rounded-lg { | |
| 725 | 729 | border-radius: 0.5rem; | |
| 726 | 730 | } | |
| @@ -733,6 +737,10 @@ video { | |||
| 733 | 737 | border-radius: 0.125rem; | |
| 734 | 738 | } | |
| 735 | 739 | ||
| 740 | + | .rounded { | |
| 741 | + | border-radius: 0.25rem; | |
| 742 | + | } | |
| 743 | + | ||
| 736 | 744 | .border { | |
| 737 | 745 | border-width: 1px; | |
| 738 | 746 | } | |
| @@ -746,6 +754,11 @@ video { | |||
| 746 | 754 | border-color: rgb(209 213 219 / var(--tw-border-opacity)); | |
| 747 | 755 | } | |
| 748 | 756 | ||
| 757 | + | .border-red-400 { | |
| 758 | + | --tw-border-opacity: 1; | |
| 759 | + | border-color: rgb(248 113 113 / var(--tw-border-opacity)); | |
| 760 | + | } | |
| 761 | + | ||
| 749 | 762 | .bg-gordonmascot-hblue { | |
| 750 | 763 | --tw-bg-opacity: 1; | |
| 751 | 764 | background-color: rgb(1 30 92 / var(--tw-bg-opacity)); | |
| @@ -761,16 +774,6 @@ video { | |||
| 761 | 774 | background-color: rgb(209 213 219 / var(--tw-bg-opacity)); | |
| 762 | 775 | } | |
| 763 | 776 | ||
| 764 | - | .bg-green-500 { | |
| 765 | - | --tw-bg-opacity: 1; | |
| 766 | - | background-color: rgb(34 197 94 / var(--tw-bg-opacity)); | |
| 767 | - | } | |
| 768 | - | ||
| 769 | - | .bg-red-500 { | |
| 770 | - | --tw-bg-opacity: 1; | |
| 771 | - | background-color: rgb(239 68 68 / var(--tw-bg-opacity)); | |
| 772 | - | } | |
| 773 | - | ||
| 774 | 777 | .bg-transparent { | |
| 775 | 778 | background-color: transparent; | |
| 776 | 779 | } | |
| @@ -785,6 +788,21 @@ video { | |||
| 785 | 788 | background-color: rgb(228 228 231 / var(--tw-bg-opacity)); | |
| 786 | 789 | } | |
| 787 | 790 | ||
| 791 | + | .bg-green-500 { | |
| 792 | + | --tw-bg-opacity: 1; | |
| 793 | + | background-color: rgb(34 197 94 / var(--tw-bg-opacity)); | |
| 794 | + | } | |
| 795 | + | ||
| 796 | + | .bg-red-500 { | |
| 797 | + | --tw-bg-opacity: 1; | |
| 798 | + | background-color: rgb(239 68 68 / var(--tw-bg-opacity)); | |
| 799 | + | } | |
| 800 | + | ||
| 801 | + | .bg-red-100 { | |
| 802 | + | --tw-bg-opacity: 1; | |
| 803 | + | background-color: rgb(254 226 226 / var(--tw-bg-opacity)); | |
| 804 | + | } | |
| 805 | + | ||
| 788 | 806 | .p-0 { | |
| 789 | 807 | padding: 0px; | |
| 790 | 808 | } | |
| @@ -807,14 +825,19 @@ video { | |||
| 807 | 825 | padding-right: 1rem; | |
| 808 | 826 | } | |
| 809 | 827 | ||
| 828 | + | .py-2 { | |
| 829 | + | padding-top: 0.5rem; | |
| 830 | + | padding-bottom: 0.5rem; | |
| 831 | + | } | |
| 832 | + | ||
| 810 | 833 | .py-1 { | |
| 811 | 834 | padding-top: 0.25rem; | |
| 812 | 835 | padding-bottom: 0.25rem; | |
| 813 | 836 | } | |
| 814 | 837 | ||
| 815 | - | .py-2 { | |
| 816 | - | padding-top: 0.5rem; | |
| 817 | - | padding-bottom: 0.5rem; | |
| 838 | + | .py-3 { | |
| 839 | + | padding-top: 0.75rem; | |
| 840 | + | padding-bottom: 0.75rem; | |
| 818 | 841 | } | |
| 819 | 842 | ||
| 820 | 843 | .align-baseline { | |
| @@ -846,14 +869,19 @@ video { | |||
| 846 | 869 | line-height: 1.25rem; | |
| 847 | 870 | } | |
| 848 | 871 | ||
| 849 | - | .font-bold { | |
| 850 | - | font-weight: 700; | |
| 872 | + | .text-xl { | |
| 873 | + | font-size: 1.25rem; | |
| 874 | + | line-height: 1.75rem; | |
| 851 | 875 | } | |
| 852 | 876 | ||
| 853 | 877 | .font-semibold { | |
| 854 | 878 | font-weight: 600; | |
| 855 | 879 | } | |
| 856 | 880 | ||
| 881 | + | .font-bold { | |
| 882 | + | font-weight: 700; | |
| 883 | + | } | |
| 884 | + | ||
| 857 | 885 | .tracking-tighter { | |
| 858 | 886 | letter-spacing: -0.05em; | |
| 859 | 887 | } | |
| @@ -878,9 +906,14 @@ video { | |||
| 878 | 906 | color: rgb(31 41 55 / var(--tw-text-opacity)); | |
| 879 | 907 | } | |
| 880 | 908 | ||
| 881 | - | .text-red-500 { | |
| 909 | + | .text-slate-100 { | |
| 882 | 910 | --tw-text-opacity: 1; | |
| 883 | - | color: rgb(239 68 68 / var(--tw-text-opacity)); | |
| 911 | + | color: rgb(241 245 249 / var(--tw-text-opacity)); | |
| 912 | + | } | |
| 913 | + | ||
| 914 | + | .text-white { | |
| 915 | + | --tw-text-opacity: 1; | |
| 916 | + | color: rgb(255 255 255 / var(--tw-text-opacity)); | |
| 884 | 917 | } | |
| 885 | 918 | ||
| 886 | 919 | .text-red-800 { | |
| @@ -888,14 +921,19 @@ video { | |||
| 888 | 921 | color: rgb(153 27 27 / var(--tw-text-opacity)); | |
| 889 | 922 | } | |
| 890 | 923 | ||
| 891 | - | .text-slate-100 { | |
| 924 | + | .text-red-700 { | |
| 892 | 925 | --tw-text-opacity: 1; | |
| 893 | - | color: rgb(241 245 249 / var(--tw-text-opacity)); | |
| 926 | + | color: rgb(185 28 28 / var(--tw-text-opacity)); | |
| 894 | 927 | } | |
| 895 | 928 | ||
| 896 | - | .text-white { | |
| 929 | + | .text-red-500 { | |
| 897 | 930 | --tw-text-opacity: 1; | |
| 898 | - | color: rgb(255 255 255 / var(--tw-text-opacity)); | |
| 931 | + | color: rgb(239 68 68 / var(--tw-text-opacity)); | |
| 932 | + | } | |
| 933 | + | ||
| 934 | + | .text-red-600 { | |
| 935 | + | --tw-text-opacity: 1; | |
| 936 | + | color: rgb(220 38 38 / var(--tw-text-opacity)); | |
| 899 | 937 | } | |
| 900 | 938 | ||
| 901 | 939 | .underline { | |
| @@ -1033,11 +1071,6 @@ video { | |||
| 1033 | 1071 | margin-right: 8px; | |
| 1034 | 1072 | } | |
| 1035 | 1073 | ||
| 1036 | - | .hover\:bg-red-700:hover { | |
| 1037 | - | --tw-bg-opacity: 1; | |
| 1038 | - | background-color: rgb(185 28 28 / var(--tw-bg-opacity)); | |
| 1039 | - | } | |
| 1040 | - | ||
| 1041 | 1074 | @media (min-width: 640px) { | |
| 1042 | 1075 | .sm\:h-24 { | |
| 1043 | 1076 | height: 6rem; | |
| @@ -575,6 +575,10 @@ video { | |||||
| 575 | } | 575 | } | ||
| 576 | } | 576 | } | ||
| 577 | 577 | ||||
| 578 | + | .relative { | |||
| 579 | + | position: relative; | |||
| 580 | + | } | |||
| 581 | + | ||||
| 578 | .my-2 { | 582 | .my-2 { | ||
| 579 | margin-top: 0.5rem; | 583 | margin-top: 0.5rem; | ||
| 580 | margin-bottom: 0.5rem; | 584 | margin-bottom: 0.5rem; | ||
| @@ -592,18 +596,22 @@ video { | |||||
| 592 | margin-right: 0.5rem; | 596 | margin-right: 0.5rem; | ||
| 593 | } | 597 | } | ||
| 594 | 598 | ||||
| 595 | - | .mt-2 { | |||
| 596 | - | margin-top: 0.5rem; | |||
| 597 | - | } | |||
| 598 | - | ||||
| 599 | .mt-4 { | 599 | .mt-4 { | ||
| 600 | margin-top: 1rem; | 600 | margin-top: 1rem; | ||
| 601 | } | 601 | } | ||
| 602 | 602 | ||||
| 603 | + | .mt-2 { | |||
| 604 | + | margin-top: 0.5rem; | |||
| 605 | + | } | |||
| 606 | + | ||||
| 603 | .flex { | 607 | .flex { | ||
| 604 | display: flex; | 608 | display: flex; | ||
| 605 | } | 609 | } | ||
| 606 | 610 | ||||
| 611 | + | .table { | |||
| 612 | + | display: table; | |||
| 613 | + | } | |||
| 614 | + | ||||
| 607 | .hidden { | 615 | .hidden { | ||
| 608 | display: none; | 616 | display: none; | ||
| 609 | } | 617 | } | ||
| @@ -636,22 +644,10 @@ video { | |||||
| 636 | width: 50%; | 644 | width: 50%; | ||
| 637 | } | 645 | } | ||
| 638 | 646 | ||||
| 639 | - | .w-1\/4 { | |||
| 640 | - | width: 25%; | |||
| 641 | - | } | |||
| 642 | - | ||||
| 643 | - | .w-1\/6 { | |||
| 644 | - | width: 16.666667%; | |||
| 645 | - | } | |||
| 646 | - | ||||
| 647 | .w-2\/3 { | 647 | .w-2\/3 { | ||
| 648 | width: 66.666667%; | 648 | width: 66.666667%; | ||
| 649 | } | 649 | } | ||
| 650 | 650 | ||||
| 651 | - | .w-2\/4 { | |||
| 652 | - | width: 50%; | |||
| 653 | - | } | |||
| 654 | - | ||||
| 655 | .w-72 { | 651 | .w-72 { | ||
| 656 | width: 18rem; | 652 | width: 18rem; | ||
| 657 | } | 653 | } | ||
| @@ -660,6 +656,18 @@ video { | |||||
| 660 | width: 100%; | 656 | width: 100%; | ||
| 661 | } | 657 | } | ||
| 662 | 658 | ||||
| 659 | + | .w-1\/4 { | |||
| 660 | + | width: 25%; | |||
| 661 | + | } | |||
| 662 | + | ||||
| 663 | + | .w-1\/6 { | |||
| 664 | + | width: 16.666667%; | |||
| 665 | + | } | |||
| 666 | + | ||||
| 667 | + | .w-2\/4 { | |||
| 668 | + | width: 50%; | |||
| 669 | + | } | |||
| 670 | + | ||||
| 663 | .min-w-fit { | 671 | .min-w-fit { | ||
| 664 | min-width: -moz-fit-content; | 672 | min-width: -moz-fit-content; | ||
| 665 | min-width: fit-content; | 673 | min-width: fit-content; | ||
| @@ -717,10 +725,6 @@ video { | |||||
| 717 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); | 725 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); | ||
| 718 | } | 726 | } | ||
| 719 | 727 | ||||
| 720 | - | .rounded { | |||
| 721 | - | border-radius: 0.25rem; | |||
| 722 | - | } | |||
| 723 | - | ||||
| 724 | .rounded-lg { | 728 | .rounded-lg { | ||
| 725 | border-radius: 0.5rem; | 729 | border-radius: 0.5rem; | ||
| 726 | } | 730 | } | ||
| @@ -733,6 +737,10 @@ video { | |||||
| 733 | border-radius: 0.125rem; | 737 | border-radius: 0.125rem; | ||
| 734 | } | 738 | } | ||
| 735 | 739 | ||||
| 740 | + | .rounded { | |||
| 741 | + | border-radius: 0.25rem; | |||
| 742 | + | } | |||
| 743 | + | ||||
| 736 | .border { | 744 | .border { | ||
| 737 | border-width: 1px; | 745 | border-width: 1px; | ||
| 738 | } | 746 | } | ||
| @@ -746,6 +754,11 @@ video { | |||||
| 746 | border-color: rgb(209 213 219 / var(--tw-border-opacity)); | 754 | border-color: rgb(209 213 219 / var(--tw-border-opacity)); | ||
| 747 | } | 755 | } | ||
| 748 | 756 | ||||
| 757 | + | .border-red-400 { | |||
| 758 | + | --tw-border-opacity: 1; | |||
| 759 | + | border-color: rgb(248 113 113 / var(--tw-border-opacity)); | |||
| 760 | + | } | |||
| 761 | + | ||||
| 749 | .bg-gordonmascot-hblue { | 762 | .bg-gordonmascot-hblue { | ||
| 750 | --tw-bg-opacity: 1; | 763 | --tw-bg-opacity: 1; | ||
| 751 | background-color: rgb(1 30 92 / var(--tw-bg-opacity)); | 764 | background-color: rgb(1 30 92 / var(--tw-bg-opacity)); | ||
| @@ -761,16 +774,6 @@ video { | |||||
| 761 | background-color: rgb(209 213 219 / var(--tw-bg-opacity)); | 774 | background-color: rgb(209 213 219 / var(--tw-bg-opacity)); | ||
| 762 | } | 775 | } | ||
| 763 | 776 | ||||
| 764 | - | .bg-green-500 { | |||
| 765 | - | --tw-bg-opacity: 1; | |||
| 766 | - | background-color: rgb(34 197 94 / var(--tw-bg-opacity)); | |||
| 767 | - | } | |||
| 768 | - | ||||
| 769 | - | .bg-red-500 { | |||
| 770 | - | --tw-bg-opacity: 1; | |||
| 771 | - | background-color: rgb(239 68 68 / var(--tw-bg-opacity)); | |||
| 772 | - | } | |||
| 773 | - | ||||
| 774 | .bg-transparent { | 777 | .bg-transparent { | ||
| 775 | background-color: transparent; | 778 | background-color: transparent; | ||
| 776 | } | 779 | } | ||
| @@ -785,6 +788,21 @@ video { | |||||
| 785 | background-color: rgb(228 228 231 / var(--tw-bg-opacity)); | 788 | background-color: rgb(228 228 231 / var(--tw-bg-opacity)); | ||
| 786 | } | 789 | } | ||
| 787 | 790 | ||||
| 791 | + | .bg-green-500 { | |||
| 792 | + | --tw-bg-opacity: 1; | |||
| 793 | + | background-color: rgb(34 197 94 / var(--tw-bg-opacity)); | |||
| 794 | + | } | |||
| 795 | + | ||||
| 796 | + | .bg-red-500 { | |||
| 797 | + | --tw-bg-opacity: 1; | |||
| 798 | + | background-color: rgb(239 68 68 / var(--tw-bg-opacity)); | |||
| 799 | + | } | |||
| 800 | + | ||||
| 801 | + | .bg-red-100 { | |||
| 802 | + | --tw-bg-opacity: 1; | |||
| 803 | + | background-color: rgb(254 226 226 / var(--tw-bg-opacity)); | |||
| 804 | + | } | |||
| 805 | + | ||||
| 788 | .p-0 { | 806 | .p-0 { | ||
| 789 | padding: 0px; | 807 | padding: 0px; | ||
| 790 | } | 808 | } | ||
| @@ -807,14 +825,19 @@ video { | |||||
| 807 | padding-right: 1rem; | 825 | padding-right: 1rem; | ||
| 808 | } | 826 | } | ||
| 809 | 827 | ||||
| 828 | + | .py-2 { | |||
| 829 | + | padding-top: 0.5rem; | |||
| 830 | + | padding-bottom: 0.5rem; | |||
| 831 | + | } | |||
| 832 | + | ||||
| 810 | .py-1 { | 833 | .py-1 { | ||
| 811 | padding-top: 0.25rem; | 834 | padding-top: 0.25rem; | ||
| 812 | padding-bottom: 0.25rem; | 835 | padding-bottom: 0.25rem; | ||
| 813 | } | 836 | } | ||
| 814 | 837 | ||||
| 815 | - | .py-2 { | 838 | + | .py-3 { |
| 816 | - | padding-top: 0.5rem; | 839 | + | padding-top: 0.75rem; |
| 817 | - | padding-bottom: 0.5rem; | 840 | + | padding-bottom: 0.75rem; |
| 818 | } | 841 | } | ||
| 819 | 842 | ||||
| 820 | .align-baseline { | 843 | .align-baseline { | ||
| @@ -846,14 +869,19 @@ video { | |||||
| 846 | line-height: 1.25rem; | 869 | line-height: 1.25rem; | ||
| 847 | } | 870 | } | ||
| 848 | 871 | ||||
| 849 | - | .font-bold { | 872 | + | .text-xl { |
| 850 | - | font-weight: 700; | 873 | + | font-size: 1.25rem; |
| 874 | + | line-height: 1.75rem; | |||
| 851 | } | 875 | } | ||
| 852 | 876 | ||||
| 853 | .font-semibold { | 877 | .font-semibold { | ||
| 854 | font-weight: 600; | 878 | font-weight: 600; | ||
| 855 | } | 879 | } | ||
| 856 | 880 | ||||
| 881 | + | .font-bold { | |||
| 882 | + | font-weight: 700; | |||
| 883 | + | } | |||
| 884 | + | ||||
| 857 | .tracking-tighter { | 885 | .tracking-tighter { | ||
| 858 | letter-spacing: -0.05em; | 886 | letter-spacing: -0.05em; | ||
| 859 | } | 887 | } | ||
| @@ -878,9 +906,14 @@ video { | |||||
| 878 | color: rgb(31 41 55 / var(--tw-text-opacity)); | 906 | color: rgb(31 41 55 / var(--tw-text-opacity)); | ||
| 879 | } | 907 | } | ||
| 880 | 908 | ||||
| 881 | - | .text-red-500 { | 909 | + | .text-slate-100 { |
| 882 | --tw-text-opacity: 1; | 910 | --tw-text-opacity: 1; | ||
| 883 | - | color: rgb(239 68 68 / var(--tw-text-opacity)); | 911 | + | color: rgb(241 245 249 / var(--tw-text-opacity)); |
| 912 | + | } | |||
| 913 | + | ||||
| 914 | + | .text-white { | |||
| 915 | + | --tw-text-opacity: 1; | |||
| 916 | + | color: rgb(255 255 255 / var(--tw-text-opacity)); | |||
| 884 | } | 917 | } | ||
| 885 | 918 | ||||
| 886 | .text-red-800 { | 919 | .text-red-800 { | ||
| @@ -888,14 +921,19 @@ video { | |||||
| 888 | color: rgb(153 27 27 / var(--tw-text-opacity)); | 921 | color: rgb(153 27 27 / var(--tw-text-opacity)); | ||
| 889 | } | 922 | } | ||
| 890 | 923 | ||||
| 891 | - | .text-slate-100 { | 924 | + | .text-red-700 { |
| 892 | --tw-text-opacity: 1; | 925 | --tw-text-opacity: 1; | ||
| 893 | - | color: rgb(241 245 249 / var(--tw-text-opacity)); | 926 | + | color: rgb(185 28 28 / var(--tw-text-opacity)); |
| 894 | } | 927 | } | ||
| 895 | 928 | ||||
| 896 | - | .text-white { | 929 | + | .text-red-500 { |
| 897 | --tw-text-opacity: 1; | 930 | --tw-text-opacity: 1; | ||
| 898 | - | color: rgb(255 255 255 / var(--tw-text-opacity)); | 931 | + | color: rgb(239 68 68 / var(--tw-text-opacity)); |
| 932 | + | } | |||
| 933 | + | ||||
| 934 | + | .text-red-600 { | |||
| 935 | + | --tw-text-opacity: 1; | |||
| 936 | + | color: rgb(220 38 38 / var(--tw-text-opacity)); | |||
| 899 | } | 937 | } | ||
| 900 | 938 | ||||
| 901 | .underline { | 939 | .underline { | ||
| @@ -1033,11 +1071,6 @@ video { | |||||
| 1033 | margin-right: 8px; | 1071 | margin-right: 8px; | ||
| 1034 | } | 1072 | } | ||
| 1035 | 1073 | ||||
| 1036 | - | .hover\:bg-red-700:hover { | |||
| 1037 | - | --tw-bg-opacity: 1; | |||
| 1038 | - | background-color: rgb(185 28 28 / var(--tw-bg-opacity)); | |||
| 1039 | - | } | |||
| 1040 | - | ||||
| 1041 | @media (min-width: 640px) { | 1074 | @media (min-width: 640px) { | ||
| 1042 | .sm\:h-24 { | 1075 | .sm\:h-24 { | ||
| 1043 | height: 6rem; | 1076 | height: 6rem; | ||
M internal/webui/public/assets/js/custom.js +0 -29
| @@ -1,29 +0,0 @@ | |||
| 1 | - | // htmx.logAll(); | |
| 2 | - | document.addEventListener('htmx:afterRequest', function(event) { | |
| 3 | - | ||
| 4 | - | // Find the error message element | |
| 5 | - | var errorMessageElement = document.getElementById('error-message-img'); | |
| 6 | - | ||
| 7 | - | // Find the image div element by ID (you need to get this ID dynamically) | |
| 8 | - | var imageDivID = 'img-' + event.detail.elt.id.replace('img-', ''); | |
| 9 | - | var imageDivElement = document.getElementById(imageDivID); | |
| 10 | - | console.log(imageDivElement); | |
| 11 | - | ||
| 12 | - | // If it's an error, we display and hide the error message | |
| 13 | - | if (event.detail.xhr.status !== 200) { | |
| 14 | - | if (errorMessageElement && errorMessageElement.textContent.trim() !== "") { | |
| 15 | - | // Set a timeout to hide it | |
| 16 | - | setTimeout(function() { | |
| 17 | - | errorMessageElement.style.display = 'none'; // or set it to empty | |
| 18 | - | errorMessageElement.textContent = ''; // clear the text content | |
| 19 | - | }, 7000); // 3000 milliseconds = 3 seconds | |
| 20 | - | } | |
| 21 | - | } else { | |
| 22 | - | // If it's a success, we hide the image div | |
| 23 | - | if (imageDivElement) { | |
| 24 | - | setTimeout(function() { | |
| 25 | - | imageDivElement.style.display = 'none'; // Hide the div | |
| 26 | - | }, 3000); // 3000 milliseconds = 3 seconds | |
| 27 | - | } | |
| 28 | - | } | |
| 29 | - | }); | |
| @@ -1,29 +0,0 @@ | |||||
| 1 | - | // htmx.logAll(); | |||
| 2 | - | document.addEventListener('htmx:afterRequest', function(event) { | |||
| 3 | - | ||||
| 4 | - | // Find the error message element | |||
| 5 | - | var errorMessageElement = document.getElementById('error-message-img'); | |||
| 6 | - | ||||
| 7 | - | // Find the image div element by ID (you need to get this ID dynamically) | |||
| 8 | - | var imageDivID = 'img-' + event.detail.elt.id.replace('img-', ''); | |||
| 9 | - | var imageDivElement = document.getElementById(imageDivID); | |||
| 10 | - | console.log(imageDivElement); | |||
| 11 | - | ||||
| 12 | - | // If it's an error, we display and hide the error message | |||
| 13 | - | if (event.detail.xhr.status !== 200) { | |||
| 14 | - | if (errorMessageElement && errorMessageElement.textContent.trim() !== "") { | |||
| 15 | - | // Set a timeout to hide it | |||
| 16 | - | setTimeout(function() { | |||
| 17 | - | errorMessageElement.style.display = 'none'; // or set it to empty | |||
| 18 | - | errorMessageElement.textContent = ''; // clear the text content | |||
| 19 | - | }, 7000); // 3000 milliseconds = 3 seconds | |||
| 20 | - | } | |||
| 21 | - | } else { | |||
| 22 | - | // If it's a success, we hide the image div | |||
| 23 | - | if (imageDivElement) { | |||
| 24 | - | setTimeout(function() { | |||
| 25 | - | imageDivElement.style.display = 'none'; // Hide the div | |||
| 26 | - | }, 3000); // 3000 milliseconds = 3 seconds | |||
| 27 | - | } | |||
| 28 | - | } | |||
| 29 | - | }); | |||
M pkg/utils/docker/client.go +9 -2
| @@ -1,7 +1,9 @@ | |||
| 1 | 1 | package docker | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 4 | 5 | "fmt" | |
| 6 | + | "log" | |
| 5 | 7 | ||
| 6 | 8 | "github.com/docker/docker/client" | |
| 7 | 9 | ) | |
| @@ -32,7 +34,8 @@ func (d *DockerClient) InitializeClient(config *Config) error { | |||
| 32 | 34 | client.WithHost("unix://"+config.Sock), // Prepend "unix://" to the Unix socket path | |
| 33 | 35 | ) | |
| 34 | 36 | if err != nil { | |
| 35 | - | return fmt.Errorf("Error from DockerClient: %s", err) | |
| 37 | + | log.Printf("Error initializing Docker client: %s", err) | |
| 38 | + | return err | |
| 36 | 39 | } | |
| 37 | 40 | dockerCli = cli | |
| 38 | 41 | return nil | |
| @@ -42,6 +45,10 @@ func CheckIfInitialized() error { | |||
| 42 | 45 | if dockerCli == nil { | |
| 43 | 46 | return fmt.Errorf("Docker client is not initialized") | |
| 44 | 47 | } | |
| 45 | - | ||
| 48 | + | // ping the Docker daemon to check if it's running | |
| 49 | + | _, err := dockerCli.Ping(context.Background()) | |
| 50 | + | if err != nil { | |
| 51 | + | return fmt.Errorf("Docker client is not initialized") | |
| 52 | + | } | |
| 46 | 53 | return nil | |
| 47 | 54 | } | |
| @@ -1,7 +1,9 @@ | |||||
| 1 | package docker | 1 | package docker | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "context" | |||
| 4 | "fmt" | 5 | "fmt" | ||
| 6 | + | "log" | |||
| 5 | 7 | ||||
| 6 | "github.com/docker/docker/client" | 8 | "github.com/docker/docker/client" | ||
| 7 | ) | 9 | ) | ||
| @@ -32,7 +34,8 @@ func (d *DockerClient) InitializeClient(config *Config) error { | |||||
| 32 | client.WithHost("unix://"+config.Sock), // Prepend "unix://" to the Unix socket path | 34 | client.WithHost("unix://"+config.Sock), // Prepend "unix://" to the Unix socket path | ||
| 33 | ) | 35 | ) | ||
| 34 | if err != nil { | 36 | if err != nil { | ||
| 35 | - | return fmt.Errorf("Error from DockerClient: %s", err) | 37 | + | log.Printf("Error initializing Docker client: %s", err) |
| 38 | + | return err | |||
| 36 | } | 39 | } | ||
| 37 | dockerCli = cli | 40 | dockerCli = cli | ||
| 38 | return nil | 41 | return nil | ||
| @@ -42,6 +45,10 @@ func CheckIfInitialized() error { | |||||
| 42 | if dockerCli == nil { | 45 | if dockerCli == nil { | ||
| 43 | return fmt.Errorf("Docker client is not initialized") | 46 | return fmt.Errorf("Docker client is not initialized") | ||
| 44 | } | 47 | } | ||
| 45 | - | 48 | + | // ping the Docker daemon to check if it's running | |
| 49 | + | _, err := dockerCli.Ping(context.Background()) | |||
| 50 | + | if err != nil { | |||
| 51 | + | return fmt.Errorf("Docker client is not initialized") | |||
| 52 | + | } | |||
| 46 | return nil | 53 | return nil | ||
| 47 | } | 54 | } | ||
M pkg/utils/docker/containers.go +3 -4
| @@ -7,6 +7,9 @@ import ( | |||
| 7 | 7 | "github.com/docker/docker/api/types" | |
| 8 | 8 | ) | |
| 9 | 9 | ||
| 10 | + | func init() { | |
| 11 | + | } | |
| 12 | + | ||
| 10 | 13 | func ListRunningContainers() ([]types.Container, error) { | |
| 11 | 14 | // Check if the Docker client has been initialized | |
| 12 | 15 | CheckIfInitialized() | |
| @@ -22,8 +25,6 @@ func ListRunningContainers() ([]types.Container, error) { | |||
| 22 | 25 | // StopContainer attempts to stop a container gracefully with a timeout | |
| 23 | 26 | // If it doesn't stop, it sets a flag that can be checked to prompt the user. | |
| 24 | 27 | func StopContainerGracefully(containerID string, timeoutDuration time.Duration) (bool, error) { | |
| 25 | - | // Check if the Docker client has been initialized | |
| 26 | - | CheckIfInitialized() | |
| 27 | 28 | ||
| 28 | 29 | // Start by sending a SIGTERM | |
| 29 | 30 | err := dockerCli.ContainerKill(context.Background(), containerID, "SIGTERM") | |
| @@ -66,8 +67,6 @@ func StopContainerRagefully(containerID string) error { | |||
| 66 | 67 | } | |
| 67 | 68 | ||
| 68 | 69 | func DeleteContainer(containerID string) error { | |
| 69 | - | // Check if the Docker client has been initialized | |
| 70 | - | CheckIfInitialized() | |
| 71 | 70 | ||
| 72 | 71 | // Delete container using the Docker client | |
| 73 | 72 | err := dockerCli.ContainerRemove(context.Background(), containerID, types.ContainerRemoveOptions{}) | |
| @@ -7,6 +7,9 @@ import ( | |||||
| 7 | "github.com/docker/docker/api/types" | 7 | "github.com/docker/docker/api/types" | ||
| 8 | ) | 8 | ) | ||
| 9 | 9 | ||||
| 10 | + | func init() { | |||
| 11 | + | } | |||
| 12 | + | ||||
| 10 | func ListRunningContainers() ([]types.Container, error) { | 13 | func ListRunningContainers() ([]types.Container, error) { | ||
| 11 | // Check if the Docker client has been initialized | 14 | // Check if the Docker client has been initialized | ||
| 12 | CheckIfInitialized() | 15 | CheckIfInitialized() | ||
| @@ -22,8 +25,6 @@ func ListRunningContainers() ([]types.Container, error) { | |||||
| 22 | // StopContainer attempts to stop a container gracefully with a timeout | 25 | // StopContainer attempts to stop a container gracefully with a timeout | ||
| 23 | // If it doesn't stop, it sets a flag that can be checked to prompt the user. | 26 | // If it doesn't stop, it sets a flag that can be checked to prompt the user. | ||
| 24 | func StopContainerGracefully(containerID string, timeoutDuration time.Duration) (bool, error) { | 27 | func StopContainerGracefully(containerID string, timeoutDuration time.Duration) (bool, error) { | ||
| 25 | - | // Check if the Docker client has been initialized | |||
| 26 | - | CheckIfInitialized() | |||
| 27 | 28 | ||||
| 28 | // Start by sending a SIGTERM | 29 | // Start by sending a SIGTERM | ||
| 29 | err := dockerCli.ContainerKill(context.Background(), containerID, "SIGTERM") | 30 | err := dockerCli.ContainerKill(context.Background(), containerID, "SIGTERM") | ||
| @@ -66,8 +67,6 @@ func StopContainerRagefully(containerID string) error { | |||||
| 66 | } | 67 | } | ||
| 67 | 68 | ||||
| 68 | func DeleteContainer(containerID string) error { | 69 | func DeleteContainer(containerID string) error { | ||
| 69 | - | // Check if the Docker client has been initialized | |||
| 70 | - | CheckIfInitialized() | |||
| 71 | 70 | ||||
| 72 | // Delete container using the Docker client | 71 | // Delete container using the Docker client | ||
| 73 | err := dockerCli.ContainerRemove(context.Background(), containerID, types.ContainerRemoveOptions{}) | 72 | err := dockerCli.ContainerRemove(context.Background(), containerID, types.ContainerRemoveOptions{}) | ||
M pkg/utils/docker/images.go +0 -1
| @@ -20,7 +20,6 @@ func ListContainerImages() ([]types.ImageSummary, error) { | |||
| 20 | 20 | } | |
| 21 | 21 | func DeleteContainerImage(imageID string) error { | |
| 22 | 22 | // Check if the Docker client has been initialized | |
| 23 | - | CheckIfInitialized() | |
| 24 | 23 | ||
| 25 | 24 | // Delete the image using the Docker client | |
| 26 | 25 | _, err := dockerCli.ImageRemove(context.Background(), imageID, types.ImageRemoveOptions{}) | |
| @@ -20,7 +20,6 @@ func ListContainerImages() ([]types.ImageSummary, error) { | |||||
| 20 | } | 20 | } | ||
| 21 | func DeleteContainerImage(imageID string) error { | 21 | func DeleteContainerImage(imageID string) error { | ||
| 22 | // Check if the Docker client has been initialized | 22 | // Check if the Docker client has been initialized | ||
| 23 | - | CheckIfInitialized() | |||
| 24 | 23 | ||||
| 25 | // Delete the image using the Docker client | 24 | // Delete the image using the Docker client | ||
| 26 | _, err := dockerCli.ImageRemove(context.Background(), imageID, types.ImageRemoveOptions{}) | 25 | _, err := dockerCli.ImageRemove(context.Background(), imageID, types.ImageRemoveOptions{}) | ||