Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
515 changes: 16 additions & 499 deletions cmd/virtwork/main.go

Large diffs are not rendered by default.

94 changes: 94 additions & 0 deletions internal/orchestrator/cleanup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2026 Red Hat
// SPDX-License-Identifier: Apache-2.0

package orchestrator

import (
"context"
"fmt"
"io"
"log/slog"

"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/opdev/virtwork/internal/audit"
"github.com/opdev/virtwork/internal/cleanup"
"github.com/opdev/virtwork/internal/config"
)

// CleanupOrchestrator coordinates the "cleanup" workflow: previewing and
// deleting managed resources. Dependencies are injected at construction
// for testability.
type CleanupOrchestrator struct {
logger *slog.Logger
client client.Client
config *config.Config
auditor audit.Auditor
writer io.Writer
}

// NewCleanupOrchestrator creates a CleanupOrchestrator with the given dependencies.
func NewCleanupOrchestrator(
logger *slog.Logger,
c client.Client,
cfg *config.Config,
auditor audit.Auditor,
writer io.Writer,
) *CleanupOrchestrator {
return &CleanupOrchestrator{
logger: logger,
client: c,
config: cfg,
auditor: auditor,
writer: writer,
}
}

// Preview returns what would be deleted without modifying any resources.
func (co *CleanupOrchestrator) Preview(
ctx context.Context,
execID int64,
runID string,
) (*cleanup.CleanupPreview, error) {
preview, err := cleanup.PreviewCleanup(ctx, co.client, co.config, runID)
if err != nil {
return nil, fmt.Errorf("previewing cleanup: %w", err)
}
return preview, nil
}

// Execute performs the cleanup, deleting managed resources and recording
// audit data.
func (co *CleanupOrchestrator) Execute(
ctx context.Context,
execID int64,
deleteNamespace bool,
runID string,
) (*cleanup.CleanupResult, error) {
_ = co.auditor.RecordEvent(ctx, execID, audit.EventRecord{
EventType: "cleanup_started",
Message: fmt.Sprintf("Starting cleanup (namespace: %s, run-id filter: %q)", co.config.Namespace, runID),
})

result, err := cleanup.CleanupAll(ctx, co.client, co.config, deleteNamespace, runID)
if err != nil {
return nil, fmt.Errorf("cleanup failed: %w", err)
}

if len(result.RunIDs) > 0 {
_ = co.auditor.LinkCleanupToRuns(ctx, execID, result.RunIDs)
}

_ = co.auditor.RecordCleanupCounts(ctx, execID,
result.VMsDeleted, result.ServicesDeleted, result.SecretsDeleted,
result.DVsDeleted, result.PVCsDeleted, result.NamespaceDeleted)

_ = co.auditor.RecordEvent(ctx, execID, audit.EventRecord{
EventType: "cleanup_completed",
Message: fmt.Sprintf("Deleted %d VMs, %d services, %d secrets, %d DVs, %d PVCs",
result.VMsDeleted, result.ServicesDeleted, result.SecretsDeleted,
result.DVsDeleted, result.PVCsDeleted),
})

return result, nil
}
183 changes: 183 additions & 0 deletions internal/orchestrator/cleanup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright 2026 Red Hat
// SPDX-License-Identifier: Apache-2.0

package orchestrator_test

import (
"bytes"
"context"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubevirtv1 "kubevirt.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/opdev/virtwork/internal/audit"
"github.com/opdev/virtwork/internal/config"
"github.com/opdev/virtwork/internal/constants"
"github.com/opdev/virtwork/internal/logging"
"github.com/opdev/virtwork/internal/orchestrator"
)

var _ = Describe("CleanupOrchestrator", func() {
var (
ctx context.Context
buf *bytes.Buffer
cfg *config.Config
auditor audit.Auditor
)

BeforeEach(func() {
ctx = context.Background()
buf = &bytes.Buffer{}
cfg = defaultConfig()
auditor = audit.NoOpAuditor{}
})

Describe("Preview", func() {
It("should return zero counts when no resources exist", func() {
c := newFakeClient()
logger := logging.NewLogger(buf, false)
co := orchestrator.NewCleanupOrchestrator(logger, c, cfg, auditor, buf)

preview, err := co.Preview(ctx, 0, "")
Expect(err).NotTo(HaveOccurred())
Expect(preview.TotalCount).To(Equal(0))
})

It("should count managed VMs", func() {
vm := &kubevirtv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "virtwork-cpu-0",
Namespace: constants.DefaultNamespace,
Labels: map[string]string{
constants.LabelManagedBy: constants.ManagedByValue,
},
},
}
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: constants.DefaultNamespace,
},
}
c := newFakeClient(ns, vm)
logger := logging.NewLogger(buf, false)
co := orchestrator.NewCleanupOrchestrator(logger, c, cfg, auditor, buf)

preview, err := co.Preview(ctx, 0, "")
Expect(err).NotTo(HaveOccurred())
Expect(preview.VMCount).To(Equal(1))
Expect(preview.TotalCount).To(BeNumerically(">=", 1))
})

It("should filter by run-id when provided", func() {
vm1 := &kubevirtv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "virtwork-cpu-0",
Namespace: constants.DefaultNamespace,
Labels: map[string]string{
constants.LabelManagedBy: constants.ManagedByValue,
constants.LabelRunID: "run-1",
},
},
}
vm2 := &kubevirtv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "virtwork-cpu-1",
Namespace: constants.DefaultNamespace,
Labels: map[string]string{
constants.LabelManagedBy: constants.ManagedByValue,
constants.LabelRunID: "run-2",
},
},
}
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: constants.DefaultNamespace,
},
}
c := newFakeClient(ns, vm1, vm2)
logger := logging.NewLogger(buf, false)
co := orchestrator.NewCleanupOrchestrator(logger, c, cfg, auditor, buf)

preview, err := co.Preview(ctx, 0, "run-1")
Expect(err).NotTo(HaveOccurred())
Expect(preview.VMCount).To(Equal(1))
})
})

Describe("Execute", func() {
It("should delete managed VMs", func() {
vm := &kubevirtv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "virtwork-cpu-0",
Namespace: constants.DefaultNamespace,
Labels: map[string]string{
constants.LabelManagedBy: constants.ManagedByValue,
},
},
}
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: constants.DefaultNamespace,
},
}
c := newFakeClient(ns, vm)
logger := logging.NewLogger(buf, false)
co := orchestrator.NewCleanupOrchestrator(logger, c, cfg, auditor, buf)

result, err := co.Execute(ctx, 0, false, "")
Expect(err).NotTo(HaveOccurred())
Expect(result.VMsDeleted).To(Equal(1))

// Verify VM is gone
vmObj := &kubevirtv1.VirtualMachine{}
err = c.Get(ctx, client.ObjectKey{
Name: "virtwork-cpu-0",
Namespace: constants.DefaultNamespace,
}, vmObj)
Expect(err).To(HaveOccurred())
})

It("should delete managed secrets", func() {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "virtwork-cpu-0-cloudinit",
Namespace: constants.DefaultNamespace,
Labels: map[string]string{
constants.LabelManagedBy: constants.ManagedByValue,
},
},
}
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: constants.DefaultNamespace,
},
}
c := newFakeClient(ns, secret)
logger := logging.NewLogger(buf, false)
co := orchestrator.NewCleanupOrchestrator(logger, c, cfg, auditor, buf)

result, err := co.Execute(ctx, 0, false, "")
Expect(err).NotTo(HaveOccurred())
Expect(result.SecretsDeleted).To(Equal(1))
})

It("should record cleanup counts in audit", func() {
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: constants.DefaultNamespace,
},
}
c := newFakeClient(ns)
logger := logging.NewLogger(buf, false)
co := orchestrator.NewCleanupOrchestrator(logger, c, cfg, auditor, buf)

result, err := co.Execute(ctx, 0, false, "")
Expect(err).NotTo(HaveOccurred())
Expect(result).NotTo(BeNil())
})
})
})
Loading
Loading