From 5a8e857ca00d3cfc6a6b20373bbeacc0107bfe8b Mon Sep 17 00:00:00 2001 From: xiaojunxiang Date: Thu, 25 Jun 2026 23:02:42 +0800 Subject: [PATCH] fix(templatecenter): clean job rows after snapshot delete Snapshot delete removed template_definition but left template_image_job rows, so ListTemplates still surfaced deleted snapshots via orphan job fallback. Mirror template delete by calling runTemplateJobCleanup after metadata cleanup, and return a terminal READY job view without re-querying deleted rows. Signed-off-by: xiaojunxiang --- CubeMaster/pkg/templatecenter/job_dto.go | 24 +++++ CubeMaster/pkg/templatecenter/job_dto_test.go | 32 +++++++ CubeMaster/pkg/templatecenter/snapshot_ops.go | 19 ++-- .../pkg/templatecenter/snapshot_ops_test.go | 93 +++++++++++++++++++ 4 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 CubeMaster/pkg/templatecenter/job_dto_test.go diff --git a/CubeMaster/pkg/templatecenter/job_dto.go b/CubeMaster/pkg/templatecenter/job_dto.go index e77cf89c3..173d08265 100644 --- a/CubeMaster/pkg/templatecenter/job_dto.go +++ b/CubeMaster/pkg/templatecenter/job_dto.go @@ -49,6 +49,30 @@ func jobModelToInfo(ctx context.Context, record *models.TemplateImageJob) (*type return info, nil } +// cloneTemplateImageJobInfo returns an independent copy of src. When +// TemplateImageJobInfo, Res, or RootfsArtifactInfo gain new pointer/slice/map +// fields, update the deep-copy branches here accordingly. +func cloneTemplateImageJobInfo(src *types.TemplateImageJobInfo) *types.TemplateImageJobInfo { + if src == nil { + return nil + } + dst := *src + dst.RedoScope = append([]string(nil), src.RedoScope...) + if src.Artifact != nil { + artifact := *src.Artifact + dst.Artifact = &artifact + } + if src.Template != nil { + template := *src.Template + if src.Template.Ret != nil { + ret := *src.Template.Ret + template.Ret = &ret + } + dst.Template = &template + } + return &dst +} + func artifactModelToInfo(record *models.RootfsArtifact) *types.RootfsArtifactInfo { return &types.RootfsArtifactInfo{ ArtifactID: record.ArtifactID, diff --git a/CubeMaster/pkg/templatecenter/job_dto_test.go b/CubeMaster/pkg/templatecenter/job_dto_test.go new file mode 100644 index 000000000..d4e8e43fc --- /dev/null +++ b/CubeMaster/pkg/templatecenter/job_dto_test.go @@ -0,0 +1,32 @@ +// Copyright (c) 2026 Tencent Inc. +// SPDX-License-Identifier: Apache-2.0 +// + +package templatecenter + +import ( + "testing" + + sandboxtypes "github.com/tencentcloud/CubeSandbox/CubeMaster/pkg/service/sandbox/types" +) + +func TestCloneTemplateImageJobInfo(t *testing.T) { + if cloneTemplateImageJobInfo(nil) != nil { + t.Fatal("nil input should return nil") + } + + src := &sandboxtypes.TemplateImageJobInfo{ + JobID: "job-1", + TemplateID: "tpl-1", + Status: JobStatusPending, + RedoScope: []string{"phase-a"}, + } + dst := cloneTemplateImageJobInfo(src) + if dst == nil || dst == src { + t.Fatal("expected independent clone") + } + dst.RedoScope[0] = "changed" + if src.RedoScope[0] == "changed" { + t.Fatal("RedoScope should be deep-copied") + } +} diff --git a/CubeMaster/pkg/templatecenter/snapshot_ops.go b/CubeMaster/pkg/templatecenter/snapshot_ops.go index e50f60417..d1a0e6fa7 100644 --- a/CubeMaster/pkg/templatecenter/snapshot_ops.go +++ b/CubeMaster/pkg/templatecenter/snapshot_ops.go @@ -708,16 +708,13 @@ func runSnapshotDeleteJob(ctx context.Context, jobID, snapshotID string) error { if err := runReplicaCleanup(ctx, snapshotID, locators); err != nil { return failSnapshotDeleteJob(ctx, jobID, snapshotID, err) } - if err := deleteSnapshotMetadataOnly(ctx, snapshotID); err != nil { + if err := runMetadataCleanup(ctx, snapshotID); err != nil { return failSnapshotDeleteJob(ctx, jobID, snapshotID, err) } invalidateTemplateCaches(snapshotID) - if err := updateTemplateImageJob(ctx, jobID, map[string]any{ - "status": JobStatusReady, - "phase": JobPhaseReady, - "progress": 100, - "result_json": `{"deleted":true}`, - }); err != nil { + // Mirror template delete: drop job rows so ListTemplates does not + // resurrect the snapshot from orphan job fallback entries. + if err := runTemplateJobCleanup(ctx, snapshotID); err != nil { return err } success = true @@ -1274,7 +1271,13 @@ func executeSnapshotDeleteJob(ctx context.Context, info *sandboxtypes.TemplateIm if err := runSnapshotDeleteJob(jobCtx, info.JobID, snapshotID); err != nil { return nil, err } - return finalizeSnapshotJobByID(ctx, info.JobID) + // runSnapshotDeleteJob removes job rows on success; return a terminal + // READY view without re-querying the deleted job record. + result := cloneTemplateImageJobInfo(info) + result.Status = JobStatusReady + result.Phase = JobPhaseReady + result.Progress = 100 + return result, nil } func resolveExistingSnapshotJobByID(ctx context.Context, jobID string) (*sandboxtypes.TemplateImageJobInfo, error) { diff --git a/CubeMaster/pkg/templatecenter/snapshot_ops_test.go b/CubeMaster/pkg/templatecenter/snapshot_ops_test.go index 54dfa3fe3..01c92681c 100644 --- a/CubeMaster/pkg/templatecenter/snapshot_ops_test.go +++ b/CubeMaster/pkg/templatecenter/snapshot_ops_test.go @@ -608,3 +608,96 @@ func TestDeleteSnapshotDoesNotBlockWhenRuntimeRefsExist(t *testing.T) { t.Fatalf("DeleteSnapshot error = %q, runtime-ref guard should no longer reject", err.Error()) } } + +func TestRunSnapshotDeleteJobCleansTemplateJobs(t *testing.T) { + origReplicaCleanup := runReplicaCleanup + origMetadataCleanup := runMetadataCleanup + origJobCleanup := runTemplateJobCleanup + t.Cleanup(func() { + runReplicaCleanup = origReplicaCleanup + runMetadataCleanup = origMetadataCleanup + runTemplateJobCleanup = origJobCleanup + }) + + patches := gomonkey.NewPatches() + defer patches.Reset() + + jobsCleaned := false + patches.ApplyFunc(updateTemplateImageJob, func(ctx context.Context, jobID string, fields map[string]any) error { + return nil + }) + patches.ApplyFunc(discoverTemplateCleanupTargets, func(ctx context.Context, templateID, instanceType string) (*templateCleanupTargets, error) { + return &templateCleanupTargets{}, nil + }) + patches.ApplyFunc(snapshotDeleteLocators, func(targets *templateCleanupTargets) ([]templateCleanupLocator, error) { + return nil, nil + }) + patches.ApplyFunc(invalidateTemplateCaches, func(templateID string) {}) + runReplicaCleanup = func(ctx context.Context, templateID string, locators []templateCleanupLocator) error { + return nil + } + runMetadataCleanup = func(ctx context.Context, templateID string) error { + return nil + } + runTemplateJobCleanup = func(ctx context.Context, templateID string) error { + if templateID != "snap-del" { + t.Fatalf("runTemplateJobCleanup templateID = %q, want snap-del", templateID) + } + jobsCleaned = true + return nil + } + + if err := runSnapshotDeleteJob(context.Background(), "job-del", "snap-del"); err != nil { + t.Fatalf("runSnapshotDeleteJob returned error: %v", err) + } + if !jobsCleaned { + t.Fatal("expected runTemplateJobCleanup to be called") + } +} + +func TestExecuteSnapshotDeleteJobReturnsReadyWithoutJobLookup(t *testing.T) { + patches := gomonkey.NewPatches() + defer patches.Reset() + + patches.ApplyFunc(claimSnapshotJobExecution, func(ctx context.Context, jobID, phase string, progress int32) (bool, error) { + return true, nil + }) + patches.ApplyFunc(runSnapshotDeleteJob, func(ctx context.Context, jobID, snapshotID string) error { + return nil + }) + getJobInfoCallCount := 0 + patches.ApplyFunc(GetTemplateImageJobInfo, func(ctx context.Context, jobID string) (*sandboxtypes.TemplateImageJobInfo, error) { + getJobInfoCallCount++ + return nil, nil + }) + + info, err := executeSnapshotDeleteJob(context.Background(), &sandboxtypes.TemplateImageJobInfo{ + JobID: "job-del", + TemplateID: "snap-del", + Status: JobStatusPending, + }, "snap-del") + if err != nil { + t.Fatalf("executeSnapshotDeleteJob returned error: %v", err) + } + if info == nil { + t.Fatal("expected non-nil job info") + } + if info.JobID != "job-del" { + t.Fatalf("jobID = %q, want %q", info.JobID, "job-del") + } + if info.TemplateID != "snap-del" { + t.Fatalf("templateID = %q, want %q", info.TemplateID, "snap-del") + } + if info.Status != JobStatusReady { + t.Fatalf("status = %q, want %q", info.Status, JobStatusReady) + } + if info.Phase != JobPhaseReady { + t.Fatalf("phase = %q, want %q", info.Phase, JobPhaseReady) + } + if info.Progress != 100 { + t.Fatalf("progress = %d, want 100", info.Progress) + } + if getJobInfoCallCount != 0 { + t.Fatalf("GetTemplateImageJobInfo called %d time(s), want 0", getJobInfoCallCount) + } +}