Skip to content
6 changes: 6 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ formatters:

settings:

gci:
sections:
- standard # standard library
- default # Third-party libraries
- localmodule # Local modules
custom-order: true
goimports:
local-prefixes:
- github.com/opdev/virtwork
Expand Down
101 changes: 63 additions & 38 deletions cmd/virtwork/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package main

import (
"context"
"errors"
"fmt"
"os"
"strings"
Expand All @@ -25,6 +26,13 @@ import (
"github.com/opdev/virtwork/internal/workloads"
)

var (
// ErrReadinessCheck: readiness check failed
ErrReadinessCheck = errors.New("readiness check failed")
// Err
ErrMultiVMWorkloadNotImplemented = errors.New("MultiVMWorkload not implemented")
)

func main() {
if err := newRootCmd().Execute(); err != nil {
os.Exit(1)
Expand Down Expand Up @@ -132,7 +140,9 @@ func runE(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("initializing auditor: %w", err)
}
defer auditor.Close()
defer func() {
_ = auditor.Close()
}()

ctx := context.Background()

Expand Down Expand Up @@ -219,7 +229,7 @@ func runE(cmd *cobra.Command, args []string) error {
return fmt.Errorf("generating cloud-init for %q: %w", name, err)
}

for i := 0; i < vmCount; i++ {
for i := range vmCount {
vmName := fmt.Sprintf("virtwork-%s-%d", name, i)
plans = append(plans, vmPlan{
workload: w,
Expand Down Expand Up @@ -249,7 +259,12 @@ func runE(cmd *cobra.Command, args []string) error {
// Multi-VM workload — use UserdataForRole
multiVM, ok := w.(workloads.MultiVMWorkload)
if !ok {
return fmt.Errorf("workload %q reports VMCount=%d but does not implement MultiVMWorkload", name, vmCount)
return fmt.Errorf(
"workload %q reports VMCount=%d; %w",
name,
vmCount,
ErrMultiVMWorkloadNotImplemented,
)
}

roles := []string{"server", "client"}
Expand All @@ -260,7 +275,7 @@ func runE(cmd *cobra.Command, args []string) error {
return fmt.Errorf("generating cloud-init for %q role %q: %w", name, role, err)
}

for i := 0; i < perRole; i++ {
for i := range perRole {
vmName := fmt.Sprintf("virtwork-%s-%s-%d", name, role, i)
labels := map[string]string{
constants.LabelAppName: fmt.Sprintf("virtwork-%s", name),
Expand Down Expand Up @@ -320,7 +335,7 @@ func runE(cmd *cobra.Command, args []string) error {
}); err != nil {
return fmt.Errorf("ensuring namespace %q: %w", cfg.Namespace, err)
}
fmt.Fprintf(cmd.OutOrStdout(), "Namespace %s ensured\n", cfg.Namespace)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Namespace %s ensured\n", cfg.Namespace)

// Create services before VMs (DNS must resolve for client VMs)
servicesCreated := 0
Expand Down Expand Up @@ -349,7 +364,7 @@ func runE(cmd *cobra.Command, args []string) error {
return fmt.Errorf("creating service for %q: %w", name, err)
}
servicesCreated++
fmt.Fprintf(cmd.OutOrStdout(), "Service %s created\n", svc.Name)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Service %s created\n", svc.Name)

_, _ = auditor.RecordResource(ctx, execID, audit.ResourceRecord{
ResourceType: "Service",
Expand Down Expand Up @@ -380,7 +395,7 @@ func runE(cmd *cobra.Command, args []string) error {
}
plans[i].vmSpec.CloudInitSecretName = secretName
secretsCreated++
fmt.Fprintf(cmd.OutOrStdout(), "Secret %s created\n", secretName)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Secret %s created\n", secretName)

_, _ = auditor.RecordResource(ctx, execID, audit.ResourceRecord{
ResourceType: "Secret",
Expand All @@ -391,8 +406,8 @@ func runE(cmd *cobra.Command, args []string) error {

// Create VMs concurrently via errgroup
g, gctx := errgroup.WithContext(ctx)
for _, p := range plans {
p := p // capture loop variable
for _, plan := range plans {
p := plan // capture loop variable
g.Go(func() error {
vmObj := vm.BuildVMSpec(*p.vmSpec)
if err := vm.CreateVM(gctx, c, vmObj); err != nil {
Expand All @@ -403,7 +418,7 @@ func runE(cmd *cobra.Command, args []string) error {
})
return fmt.Errorf("creating VM %q: %w", p.vmName, err)
}
fmt.Fprintf(cmd.OutOrStdout(), "VM %s created\n", p.vmName)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "VM %s created\n", p.vmName)

wlID := auditWorkloadIDs[p.component]
_, _ = auditor.RecordVM(ctx, execID, wlID, audit.VMRecord{
Expand Down Expand Up @@ -431,15 +446,15 @@ func runE(cmd *cobra.Command, args []string) error {
// Wait for readiness
if cfg.WaitForReady {
timeout := time.Duration(cfg.ReadyTimeoutSeconds) * time.Second
fmt.Fprintf(cmd.OutOrStdout(), "Waiting for %d VMs to become ready (timeout: %s)...\n",
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Waiting for %d VMs to become ready (timeout: %s)...\n",
len(vmNames), timeout)
results := wait.WaitForAllVMsReady(ctx, c, vmNames, cfg.Namespace,
timeout, constants.DefaultPollInterval)

failures := 0
for name, err := range results {
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "VM %s: %v\n", name, err)
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "VM %s: %v\n", name, err)
failures++
_ = auditor.RecordEvent(ctx, execID, audit.EventRecord{
EventType: "vm_timeout",
Expand All @@ -454,10 +469,9 @@ func runE(cmd *cobra.Command, args []string) error {
}
}
if failures > 0 {
err = fmt.Errorf("%d of %d VMs failed readiness check", failures, len(vmNames))
return err
return fmt.Errorf("%d of %d VMs failed; %w", failures, len(vmNames), ErrReadinessCheck)
}
fmt.Fprintf(cmd.OutOrStdout(), "All %d VMs ready\n", len(vmNames))
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "All %d VMs ready\n", len(vmNames))
}

// Mark all workloads as created
Expand Down Expand Up @@ -486,12 +500,14 @@ func cleanupE(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("initializing auditor: %w", err)
}
defer auditor.Close()
defer func() {
_ = auditor.Close()
}()

ctx := context.Background()

// Start audit execution
var dryRunBanner string = ""
dryRunBanner := ""
cmdName := "cleanup"
if cfg.DryRun {
// Log the command being executed
Expand Down Expand Up @@ -547,17 +563,17 @@ func cleanupE(cmd *cobra.Command, args []string) error {
_ = auditor.CompleteExecution(ctx, execID, "success", "")
err = nil // clear for defer

fmt.Fprintf(cmd.OutOrStdout(), "%sCleanup complete: %d VMs deleted, %d services deleted, %d secrets deleted",
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%sCleanup complete: %d VMs deleted, %d services deleted, %d secrets deleted",
dryRunBanner, result.VMsDeleted, result.ServicesDeleted, result.SecretsDeleted)
if result.NamespaceDeleted {
fmt.Fprintf(cmd.OutOrStdout(), ", namespace deleted")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), ", namespace deleted")
}
fmt.Fprintln(cmd.OutOrStdout())
_, _ = fmt.Fprintln(cmd.OutOrStdout())

if len(result.Errors) > 0 {
fmt.Fprintf(cmd.ErrOrStderr(), "Warnings (%d):\n", len(result.Errors))
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Warnings (%d):\n", len(result.Errors))
for _, e := range result.Errors {
fmt.Fprintf(cmd.ErrOrStderr(), " - %v\n", e)
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), " - %v\n", e)
}
}

Expand All @@ -566,33 +582,42 @@ func cleanupE(cmd *cobra.Command, args []string) error {

// printDryRun outputs VM specs in YAML without connecting to a cluster.
func printDryRun(plans []vmPlan) error {
fmt.Println("--- Dry Run ---")
fmt.Printf("Total VMs to create: %d\n\n", len(plans))
_, _ = fmt.Printf("--- Dry Run ---\nTotal VMs to create: %d\n\n", len(plans))

for _, p := range plans {
vmObj := vm.BuildVMSpec(*p.vmSpec)
data, err := sigyaml.Marshal(vmObj)
if err != nil {
return fmt.Errorf("marshaling VM spec for %q: %w", p.vmName, err)
}
fmt.Printf("# VM: %s (workload: %s)\n", p.vmName, p.component)
fmt.Println(string(data))
fmt.Println("---")
_, _ = fmt.Printf("# VM: %s (workload: %s)\n%s\n%s\n", p.vmName, p.component, string(data), "---")
}
return nil
}

// printSummary outputs a deployment summary table.
func printSummary(cmd *cobra.Command, vmCount, svcCount, secCount int, cfg *config.Config, runID string) {
out := cmd.OutOrStdout()
fmt.Fprintln(out, strings.Repeat("=", 50))
fmt.Fprintln(out, "Deployment Summary")
fmt.Fprintln(out, strings.Repeat("=", 50))
fmt.Fprintf(out, "Run ID: %s\n", runID)
fmt.Fprintf(out, "Namespace: %s\n", cfg.Namespace)
fmt.Fprintf(out, "VMs created: %d\n", vmCount)
fmt.Fprintf(out, "Services: %d\n", svcCount)
fmt.Fprintf(out, "Secrets: %d\n", secCount)
fmt.Fprintf(out, "Image: %s\n", cfg.ContainerDiskImage)
fmt.Fprintln(out, strings.Repeat("=", 50))
summaryTemplate := `%s
Deployment Summary
%s
Run ID: %s
Namespace: %s
VMs created: %d
Services: %d
Secrets: %d
Image: %s
%s`
_, _ = fmt.Fprintf(
cmd.OutOrStdout(),
summaryTemplate,
strings.Repeat("=", 50),
strings.Repeat("=", 50),
runID,
cfg.Namespace,
vmCount,
svcCount,
secCount,
cfg.ContainerDiskImage,
strings.Repeat("=", 50),
)
}
39 changes: 30 additions & 9 deletions internal/audit/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ type Auditor interface {
// LinkCleanupToRuns sets linked_run_ids on a cleanup audit_log row.
LinkCleanupToRuns(ctx context.Context, cleanupID int64, runIDs []string) error
// RecordCleanupCounts updates cleanup-specific counters on the audit_log row.
RecordCleanupCounts(ctx context.Context, id int64, vmsDeleted, servicesDeleted, secretsDeleted int, namespaceDeleted bool) error
RecordCleanupCounts(
ctx context.Context,
id int64,
vmsDeleted, servicesDeleted, secretsDeleted int,
namespaceDeleted bool,
) error

// RecordWorkload inserts a workload_details row.
RecordWorkload(ctx context.Context, executionID int64, w WorkloadRecord) (workloadID int64, err error)
Expand Down Expand Up @@ -64,7 +69,7 @@ type SQLiteAuditor struct {
func NewSQLiteAuditor(dbPath string) (*SQLiteAuditor, error) {
dir := filepath.Dir(dbPath)
if dir != "." && dir != "" {
if err := os.MkdirAll(dir, 0o755); err != nil {
if err := os.MkdirAll(dir, 0o750); err != nil {
return nil, fmt.Errorf("creating audit db directory: %w", err)
}
}
Expand All @@ -79,7 +84,7 @@ func NewSQLiteAuditor(dbPath string) (*SQLiteAuditor, error) {
}

if _, err := db.Exec(schemaSQL); err != nil {
db.Close()
_ = db.Close()
return nil, fmt.Errorf("applying audit schema: %w", err)
}

Expand Down Expand Up @@ -146,10 +151,21 @@ func (a *SQLiteAuditor) LinkCleanupToRuns(ctx context.Context, cleanupID int64,
return err
}

func (a *SQLiteAuditor) RecordCleanupCounts(ctx context.Context, id int64, vmsDeleted, servicesDeleted, secretsDeleted int, namespaceDeleted bool) error {
_, err := a.db.ExecContext(ctx,
func (a *SQLiteAuditor) RecordCleanupCounts(
ctx context.Context,
id int64,
vmsDeleted, servicesDeleted, secretsDeleted int,
namespaceDeleted bool,
) error {
_, err := a.db.ExecContext(
ctx,
`UPDATE audit_log SET vms_deleted = ?, services_deleted = ?, secrets_deleted = ?, namespace_deleted = ? WHERE id = ?`,
vmsDeleted, servicesDeleted, secretsDeleted, boolToInt(namespaceDeleted), id)
vmsDeleted,
servicesDeleted,
secretsDeleted,
boolToInt(namespaceDeleted),
id,
)
return err
}

Expand Down Expand Up @@ -256,11 +272,16 @@ type NoOpAuditor struct{}
func (NoOpAuditor) StartExecution(_ context.Context, _ string, _ *config.Config) (int64, string, error) {
return 0, "", nil
}
func (NoOpAuditor) CompleteExecution(_ context.Context, _ int64, _ string, _ string) error { return nil }
func (NoOpAuditor) LinkCleanupToRuns(_ context.Context, _ int64, _ []string) error { return nil }

func (NoOpAuditor) CompleteExecution(_ context.Context, _ int64, _ string, _ string) error {
return nil
}

func (NoOpAuditor) LinkCleanupToRuns(_ context.Context, _ int64, _ []string) error { return nil }
func (NoOpAuditor) RecordCleanupCounts(_ context.Context, _ int64, _, _, _ int, _ bool) error {
return nil
}

func (NoOpAuditor) RecordWorkload(_ context.Context, _ int64, _ WorkloadRecord) (int64, error) {
return 0, nil
}
Expand All @@ -269,7 +290,7 @@ func (NoOpAuditor) RecordVM(_ context.Context, _ int64, _ int64, _ VMRecord) (in
return 0, nil
}
func (NoOpAuditor) UpdateVMStatus(_ context.Context, _ int64, _ string, _ string) error { return nil }
func (NoOpAuditor) RecordVMDeletion(_ context.Context, _ int64) error { return nil }
func (NoOpAuditor) RecordVMDeletion(_ context.Context, _ int64) error { return nil }
func (NoOpAuditor) RecordResource(_ context.Context, _ int64, _ ResourceRecord) (int64, error) {
return 0, nil
}
Expand Down
9 changes: 4 additions & 5 deletions internal/audit/audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,7 @@ var _ = Describe("SQLiteAuditor", func() {
})

Describe("execution lifecycle", func() {
var (
cfg *config.Config
)
var cfg *config.Config

BeforeEach(func() {
cfg = &config.Config{
Expand Down Expand Up @@ -306,7 +304,8 @@ var _ = Describe("SQLiteAuditor", func() {
db := auditor.DB()
var status string
var deletedAt sql.NullString
err = db.QueryRow(`SELECT status, deleted_at FROM resource_details WHERE id = ?`, resID).Scan(&status, &deletedAt)
err = db.QueryRow(`SELECT status, deleted_at FROM resource_details WHERE id = ?`, resID).
Scan(&status, &deletedAt)
Expect(err).NotTo(HaveOccurred())
Expect(status).To(Equal("deleted"))
Expect(deletedAt.Valid).To(BeTrue())
Expand Down Expand Up @@ -361,7 +360,7 @@ var _ = Describe("SQLiteAuditor", func() {

var wg sync.WaitGroup
errs := make([]error, 20)
for i := 0; i < 20; i++ {
for i := range 20 {
wg.Add(1)
go func(idx int) {
defer wg.Done()
Expand Down
14 changes: 7 additions & 7 deletions internal/audit/records.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ package audit

// WorkloadRecord holds data for inserting a workload_details row.
type WorkloadRecord struct {
WorkloadType string
Enabled bool
VMCount int
CPUCores int
Memory string
HasDataDisk bool
DataDiskSize string
WorkloadType string
Enabled bool
VMCount int
CPUCores int
Memory string
HasDataDisk bool
DataDiskSize string
RequiresService bool
}

Expand Down
Loading