diff --git a/cmd/virtwork/main.go b/cmd/virtwork/main.go index 21fd7e0..407da5b 100644 --- a/cmd/virtwork/main.go +++ b/cmd/virtwork/main.go @@ -530,9 +530,18 @@ func runE(cmd *cobra.Command, args []string) error { }) } if err := g.Wait(); err != nil { + for _, wlID := range auditWorkloadIDs { + _ = auditor.UpdateWorkloadStatus(ctx, wlID, "failed") + } return fmt.Errorf("creating VMs: %w", err) } + // Build VM-name → workload-component mapping for readiness tracking + vmToComponent := make(map[string]string, len(plans)) + for _, p := range plans { + vmToComponent[p.vmName] = p.component + } + // Wait for readiness if cfg.WaitForReady { timeout := time.Duration(cfg.ReadyTimeoutSeconds) * time.Second @@ -543,6 +552,7 @@ func runE(cmd *cobra.Command, args []string) error { timeout, constants.DefaultPollInterval) failures := 0 + failedWorkloads := make(map[string]bool) for name, err := range results { if err != nil { logger.Error("vm readiness check failed", @@ -554,6 +564,9 @@ func runE(cmd *cobra.Command, args []string) error { Message: fmt.Sprintf("VM %s failed readiness check", name), ErrorDetail: err.Error(), }) + if comp, ok := vmToComponent[name]; ok { + failedWorkloads[comp] = true + } } else { _ = auditor.RecordEvent(ctx, execID, audit.EventRecord{ EventType: "vm_ready", @@ -562,6 +575,13 @@ func runE(cmd *cobra.Command, args []string) error { } } if failures > 0 { + for comp, wlID := range auditWorkloadIDs { + if failedWorkloads[comp] { + _ = auditor.UpdateWorkloadStatus(ctx, wlID, "failed") + } else { + _ = auditor.UpdateWorkloadStatus(ctx, wlID, "created") + } + } return fmt.Errorf("%d of %d VMs failed; %w", failures, len(vmNames), ErrReadinessCheck) } logger.Info("all VMs ready", slog.Int("vm_count", len(vmNames))) diff --git a/docs/audit-schema.md b/docs/audit-schema.md index 35c97b4..fbc154c 100644 --- a/docs/audit-schema.md +++ b/docs/audit-schema.md @@ -135,7 +135,7 @@ Indexes: `started_at`, `namespace`, `status`, `run_id`. | `has_data_disk` | INTEGER NOT NULL | 1 when `DataVolumeTemplates()` is non-empty | | `data_disk_size` | TEXT | NULL when `has_data_disk = 0` | | `requires_service` | INTEGER NOT NULL | 1 for multi-VM workloads (network, tps) | -| `status` | TEXT NOT NULL | `pending` → `created` (after VM create succeeds) → `failed` | +| `status` | TEXT NOT NULL | `pending` → `created` (after all VMs succeed) or `failed` (on VM creation or readiness error) | Indexes: `audit_id`, `workload_type`. diff --git a/internal/audit/audit_test.go b/internal/audit/audit_test.go index da1db32..9b5b4b5 100644 --- a/internal/audit/audit_test.go +++ b/internal/audit/audit_test.go @@ -562,3 +562,60 @@ var _ = Describe("NoOpAuditor", func() { Expect(a.Close()).To(Succeed()) }) }) + +var _ = Describe("Workload status transitions", func() { + var ( + auditor *audit.SQLiteAuditor + ctx context.Context + execID int64 + wlID int64 + ) + + BeforeEach(func() { + ctx = context.Background() + var err error + auditor, err = audit.NewSQLiteAuditor(":memory:") + Expect(err).NotTo(HaveOccurred()) + + cfg := &config.Config{Namespace: "test-ns"} + execID, _, err = auditor.StartExecution(ctx, "run", cfg) + Expect(err).NotTo(HaveOccurred()) + + wlID, err = auditor.RecordWorkload(ctx, execID, audit.WorkloadRecord{ + WorkloadType: "cpu", Enabled: true, VMCount: 1, CPUCores: 2, Memory: "2Gi", + }) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + Expect(auditor.Close()).To(Succeed()) + }) + + It("defaults to pending on insert", func() { + db := auditor.DB() + var status string + err := db.QueryRow(`SELECT status FROM workload_details WHERE id = ?`, wlID).Scan(&status) + Expect(err).NotTo(HaveOccurred()) + Expect(status).To(Equal("pending")) + }) + + It("transitions to created on success", func() { + Expect(auditor.UpdateWorkloadStatus(ctx, wlID, "created")).To(Succeed()) + + db := auditor.DB() + var status string + err := db.QueryRow(`SELECT status FROM workload_details WHERE id = ?`, wlID).Scan(&status) + Expect(err).NotTo(HaveOccurred()) + Expect(status).To(Equal("created")) + }) + + It("transitions to failed on orchestration error", func() { + Expect(auditor.UpdateWorkloadStatus(ctx, wlID, "failed")).To(Succeed()) + + db := auditor.DB() + var status string + err := db.QueryRow(`SELECT status FROM workload_details WHERE id = ?`, wlID).Scan(&status) + Expect(err).NotTo(HaveOccurred()) + Expect(status).To(Equal("failed")) + }) +})