From 021603d25deb6839025c10908b75f5a828d861ef Mon Sep 17 00:00:00 2001 From: ssongliu <73214554+ssongliu@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:50:15 +0800 Subject: [PATCH] fix: Optimize container compose deletion logic (#10186) Refs #10184 --- agent/app/dto/container.go | 1 + agent/app/service/container_compose.go | 37 ++++++++++++++++--- frontend/src/api/interface/container.ts | 1 + .../views/container/compose/delete/index.vue | 9 +++++ .../src/views/container/compose/index.vue | 1 + 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/agent/app/dto/container.go b/agent/app/dto/container.go index 5f654ddb9..69dbc9de9 100644 --- a/agent/app/dto/container.go +++ b/agent/app/dto/container.go @@ -260,6 +260,7 @@ type ComposeOperation struct { Path string `json:"path"` Operation string `json:"operation" validate:"required,oneof=up start restart stop down delete"` WithFile bool `json:"withFile"` + Force bool `josn:"force"` } type ComposeUpdate struct { Name string `json:"name" validate:"required"` diff --git a/agent/app/service/container_compose.go b/agent/app/service/container_compose.go index 04457b8b0..add4433cd 100644 --- a/agent/app/service/container_compose.go +++ b/agent/app/service/container_compose.go @@ -227,12 +227,9 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error { if cmd.CheckIllegal(req.Path, req.Operation) { return buserr.New("ErrCmdIllegal") } - if _, err := os.Stat(req.Path); err != nil { - return fmt.Errorf("load file with path %s failed, %v", req.Path, err) - } if req.Operation == "delete" { - if stdout, err := compose.Operate(req.Path, "down"); err != nil { - return errors.New(string(stdout)) + if err := removeContainerForCompose(req.Name, req.Path); err != nil && !req.Force { + return err } if req.WithFile { _ = os.RemoveAll(path.Dir(req.Path)) @@ -240,6 +237,9 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error { _ = composeRepo.DeleteRecord(repo.WithByName(req.Name)) return nil } + if _, err := os.Stat(req.Path); err != nil { + return fmt.Errorf("load file with path %s failed, %v", req.Path, err) + } if req.Operation == "up" { if stdout, err := compose.Up(req.Path); err != nil { return errors.New(string(stdout)) @@ -308,6 +308,33 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error { return nil } +func removeContainerForCompose(composeName, composePath string) error { + if _, err := os.Stat(composePath); err == nil { + if stdout, err := compose.Operate(composePath, "down"); err != nil { + return errors.New(stdout) + } + return nil + } + var options container.ListOptions + options.All = true + options.Filters = filters.NewArgs() + options.Filters.Add("label", "com.docker.compose.project="+composeName) + client, err := docker.NewDockerClient() + if err != nil { + return err + } + defer client.Close() + ctx := context.Background() + containers, err := client.ContainerList(ctx, options) + if err != nil { + return err + } + for _, c := range containers { + _ = client.ContainerRemove(ctx, c.ID, container.RemoveOptions{RemoveVolumes: true, Force: true}) + } + return nil +} + func recreateCompose(content, path string) error { file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640) if err != nil { diff --git a/frontend/src/api/interface/container.ts b/frontend/src/api/interface/container.ts index 538606cca..930fb85cb 100644 --- a/frontend/src/api/interface/container.ts +++ b/frontend/src/api/interface/container.ts @@ -309,6 +309,7 @@ export namespace Container { operation: string; path: string; withFile: boolean; + force: boolean; } export interface ComposeUpdate { name: string; diff --git a/frontend/src/views/container/compose/delete/index.vue b/frontend/src/views/container/compose/delete/index.vue index 21dd56c72..25a8c9c81 100644 --- a/frontend/src/views/container/compose/delete/index.vue +++ b/frontend/src/views/container/compose/delete/index.vue @@ -7,6 +7,12 @@ {{ $t('container.deleteComposeHelper') }} + + + + {{ $t('website.forceDeleteHelper') }} + +
{{ $t('database.delete') }} @@ -40,6 +46,7 @@ let loading = ref(false); let deleteInfo = ref(''); const deleteFile = ref(); +const force = ref(); const composeName = ref(); const composePath = ref(); @@ -53,6 +60,7 @@ const emit = defineEmits<{ (e: 'search'): void }>(); const acceptParams = async (prop: DialogProps) => { deleteFile.value = false; + force.value = false; composeName.value = prop.name; composePath.value = prop.path; deleteInfo.value = ''; @@ -66,6 +74,7 @@ const submit = async () => { path: composePath.value, operation: 'delete', withFile: deleteFile.value, + force: force.value, }; await composeOperator(params) .then(() => { diff --git a/frontend/src/views/container/compose/index.vue b/frontend/src/views/container/compose/index.vue index 1eaa4de91..7e2ee9cfa 100644 --- a/frontend/src/views/container/compose/index.vue +++ b/frontend/src/views/container/compose/index.vue @@ -203,6 +203,7 @@ const onComposeOperate = async (operation: string, row: any) => { path: row.path, operation: operation, withFile: false, + force: false, }; loading.value = true; await composeOperator(params)