1
0
mirror of https://github.com/amir20/dozzle.git synced 2026-01-03 03:27:29 +01:00

feat: can start and stop containers now using the drop down. This feature is not enabled by default. (#2548)

This commit is contained in:
Akash Ramaswamy
2023-12-01 03:34:22 +05:30
committed by GitHub
parent 050e499f8e
commit f78534f529
11 changed files with 160 additions and 2 deletions

View File

@@ -183,6 +183,7 @@ declare global {
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useContainerActions: typeof import('./composable/containerActions')['useContainerActions']
const useContainerContext: typeof import('./composable/containerContext')['useContainerContext']
const useContainerStore: typeof import('./stores/container')['useContainerStore']
const useCounter: typeof import('@vueuse/core')['useCounter']
@@ -533,6 +534,7 @@ declare module 'vue' {
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useContainerActions: UnwrapRef<typeof import('./composable/containerActions')['useContainerActions']>
readonly useContainerContext: UnwrapRef<typeof import('./composable/containerContext')['useContainerContext']>
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
@@ -876,6 +878,7 @@ declare module '@vue/runtime-core' {
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useContainerActions: UnwrapRef<typeof import('./composable/containerActions')['useContainerActions']>
readonly useContainerContext: UnwrapRef<typeof import('./composable/containerContext')['useContainerContext']>
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>

View File

@@ -12,8 +12,11 @@ declare module 'vue' {
'Carbon:circleSolid': typeof import('~icons/carbon/circle-solid')['default']
'Carbon:information': typeof import('~icons/carbon/information')['default']
'Carbon:macShift': typeof import('~icons/carbon/mac-shift')['default']
'Carbon:play': typeof import('~icons/carbon/play')['default']
'Carbon:restart': typeof import('~icons/carbon/restart')['default']
'Carbon:star': typeof import('~icons/carbon/star')['default']
'Carbon:starFilled': typeof import('~icons/carbon/star-filled')['default']
'Carbon:stopFilledAlt': typeof import('~icons/carbon/stop-filled-alt')['default']
'Carbon:warning': typeof import('~icons/carbon/warning')['default']
'Cil:checkCircle': typeof import('~icons/cil/check-circle')['default']
'Cil:circle': typeof import('~icons/cil/circle')['default']

View File

@@ -4,7 +4,7 @@
<carbon:circle-solid class="w-2.5 text-red" v-if="streamConfig.stderr" />
<carbon:circle-solid class="w-2.5 text-blue" v-if="streamConfig.stdout" />
</label>
<ul tabindex="0" class="menu dropdown-content rounded-box z-50 w-52 bg-base p-1 shadow">
<ul tabindex="0" class="menu dropdown-content z-50 w-52 rounded-box bg-base p-1 shadow">
<li>
<a @click.prevent="clear()">
<octicon:trash-24 /> {{ $t("toolbar.clear") }}
@@ -65,17 +65,57 @@
{{ $t("toolbar.show", { std: "STDERR" }) }}
</a>
</li>
<!-- Container Actions (Enabled via config) -->
<template v-if="enableActions">
<li class="line"></li>
<li>
<button
@click="stop()"
:disabled="actionStates.stop || actionStates.restart"
v-if="container.state == 'running'"
>
<carbon:stop-filled-alt /> {{ $t("toolbar.stop") }}
</button>
<button
@click="start()"
:disabled="actionStates.start || actionStates.restart"
v-if="container.state != 'running'"
>
<carbon:play /> {{ $t("toolbar.start") }}
</button>
</li>
<li>
<button @click="restart()" :disabled="disableRestart">
<carbon:restart
:class="{
'animate-spin': actionStates.restart,
'text-secondary': actionStates.restart,
}"
/>
{{ $t("toolbar.restart") }}
</button>
</li>
</template>
</ul>
</div>
</template>
<script lang="ts" setup>
const { showSearch } = useSearchFilter();
const { base } = config;
const { base, enableActions } = config;
const clear = defineEmit();
const { container, streamConfig } = useContainerContext();
// container context is provided in the parent component: <LogContainer>
const { actionStates, start, stop, restart } = useContainerActions();
const disableRestart = computed(() => {
return actionStates.stop || actionStates.start || actionStates.restart;
});
</script>
<style scoped lang="postcss">

View File

@@ -0,0 +1,45 @@
type ContainerActions = "start" | "stop" | "restart";
export const useContainerActions = () => {
const { container } = useContainerContext();
const { showToast } = useToast();
const actionStates = reactive({
stop: false,
restart: false,
start: false,
});
async function actionHandler(action: ContainerActions) {
const actionUrl = `/api/actions/${action}/${container.value.host}/${container.value.id}`;
const errors = {
404: "container not found",
500: "unable to complete action",
400: "invalid action",
} as Record<number, string>;
const defaultError = "something went wrong";
const toastTitle = "Action Failed";
actionStates[action] = true;
try {
const response = await fetch(withBase(actionUrl), { method: "POST" });
if (!response.ok) {
const message = errors[response.status] ?? defaultError;
showToast({ type: "error", message, title: toastTitle });
}
} catch (error) {
showToast({ type: "error", message: defaultError, title: toastTitle });
}
actionStates[action] = false;
}
return {
actionStates,
start: () => actionHandler("start"),
stop: () => actionHandler("stop"),
restart: () => actionHandler("restart"),
};
};

View File

@@ -11,6 +11,7 @@ export interface Config {
hostname: string;
hosts: { name: string; id: string }[];
authProvider: "simple" | "none" | "forward-proxy";
enableActions: boolean;
user?: {
username: string;
email: string;

View File

@@ -13,6 +13,7 @@ import (
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
@@ -49,6 +50,9 @@ type DockerCLI interface {
ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error)
ContainerStats(ctx context.Context, containerID string, stream bool) (types.ContainerStats, error)
Ping(ctx context.Context) (types.Ping, error)
ContainerStart(ctx context.Context, containerID string, options types.ContainerStartOptions) error
ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error
ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error
}
type Client struct {
@@ -145,6 +149,19 @@ func (d *Client) FindContainer(id string) (Container, error) {
return container, nil
}
func (d *Client) ContainerActions(action string, id string) error {
switch action {
case "start":
return d.cli.ContainerStart(context.Background(), id, types.ContainerStartOptions{})
case "stop":
return d.cli.ContainerStop(context.Background(), id, container.StopOptions{})
case "restart":
return d.cli.ContainerRestart(context.Background(), id, container.StopOptions{})
default:
return fmt.Errorf("unknown action: %s", action)
}
}
func (d *Client) ListContainers() ([]Container, error) {
containerListOptions := types.ContainerListOptions{
Filters: d.filters,

View File

@@ -0,0 +1,40 @@
package web
import (
"net/http"
"github.com/go-chi/chi/v5"
log "github.com/sirupsen/logrus"
)
func (h *handler) containerActions(w http.ResponseWriter, r *http.Request) {
action := chi.URLParam(r, "action")
id := chi.URLParam(r, "id")
log.Debugf("container action: %s, container id: %s", action, id)
client := h.clientFromRequest(r)
if client == nil {
log.Errorf("no client found for host %v", r.URL)
w.WriteHeader(http.StatusBadRequest)
return
}
container, err := client.FindContainer(id)
if err != nil {
log.Error(err)
w.WriteHeader(http.StatusNotFound)
return
}
err = client.ContainerActions(action, container.ID)
if err != nil {
log.Errorf("error while trying to perform action: %s", action)
w.WriteHeader(http.StatusInternalServerError)
return
}
log.Infof("container action performed: %s; container id: %s", action, id)
w.WriteHeader(http.StatusOK)
}

View File

@@ -51,6 +51,7 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
"hostname": h.config.Hostname,
"hosts": hosts,
"authProvider": h.config.Authorization.Provider,
"enableActions": h.config.EnableActions,
}
pages, err := content.ReadAll()

View File

@@ -35,6 +35,7 @@ type Config struct {
NoAnalytics bool
Dev bool
Authorization Authorization
EnableActions bool
}
type Authorization struct {
@@ -63,6 +64,7 @@ type DockerClient interface {
ContainerStats(context.Context, string, chan<- docker.ContainerStat) error
Ping(context.Context) (types.Ping, error)
Host() *docker.Host
ContainerActions(action string, id string) error
}
func CreateServer(clients map[string]DockerClient, content fs.FS, config Config) *http.Server {
@@ -104,6 +106,7 @@ func createRouter(h *handler) *chi.Mux {
r.Get("/api/logs/download/{host}/{id}", h.downloadLogs)
r.Get("/api/logs/{host}/{id}", h.fetchLogsBetweenDates)
r.Get("/api/events/stream", h.streamEvents)
r.Post("/api/actions/{action}/{host}/{id}", h.containerActions)
r.Get("/api/releases", h.releases)
r.Patch("/api/profile", h.updateProfile)
r.Get("/api/content/{id}", h.staticContent)

View File

@@ -4,6 +4,9 @@ toolbar:
search: Search
show: Show only {std}
show-all: Show all streams
stop: Stop
start: Start
restart: Restart
label:
containers: Containers
total-containers: Total Containers

View File

@@ -53,6 +53,7 @@ type args struct {
Healthcheck *HealthcheckCmd `arg:"subcommand:healthcheck" help:"checks if the server is running."`
RemoteHost []string `arg:"env:DOZZLE_REMOTE_HOST,--remote-host,separate" help:"list of hosts to connect remotely"`
AuthProvider string `arg:"--auth-provider,env:DOZZLE_AUTH_PROVIDER" default:"none" help:"sets the auth provider to use. Currently only forward-proxy is supported."`
EnableActions bool `arg:"env:DOZZLE_ENABLE_ACTIONS" default:"false" help:"enables essential actions on containers from the web interface."`
}
type HealthcheckCmd struct {
@@ -212,6 +213,7 @@ func createServer(args args, clients map[string]web.DockerClient) *http.Server {
Provider: provider,
Authorizer: authorizer,
},
EnableActions: args.EnableActions,
}
assets, err := fs.Sub(content, "dist")