From 57de20345ca547b305c870cac8069e5e9789dd2b Mon Sep 17 00:00:00 2001 From: Melvin Hillsman Date: Mon, 1 Jun 2026 12:49:22 -0500 Subject: [PATCH 1/5] docs: fix Go version and add orchestrator to project structure Update Go prerequisite from 1.25+ to 1.26+ to match go.mod. Add missing orchestrator/ package to project layout listing. Signed-off-by: Melvin Hillsman --- docs/development.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/development.md b/docs/development.md index ccf5747..3407f7b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -4,7 +4,7 @@ ### Prerequisites -- Go 1.25+ +- Go 1.26+ - [Ginkgo CLI](https://onsi.github.io/ginkgo/#installing-ginkgo) (for BDD test runner) ### Install Ginkgo CLI @@ -427,6 +427,7 @@ internal/ # Application packages (not importable externally) wait/ # VMI readiness polling (errgroup) cleanup/ # Label-based teardown (VMs, Services, Secrets) audit/ # SQLite audit tracking (Auditor interface, schema, records) + orchestrator/ # Run + Cleanup orchestrators (errgroup concurrency, VM creation flow) workloads/ # Workload + MultiVMWorkload interfaces, nine implementations, registry testutil/ # Shared test helpers for integration and E2E tests tests/ # Tests requiring external infrastructure From 2b07ade47be46ce78a8d0a87324719770a1156ea Mon Sep 17 00:00:00 2001 From: Melvin Hillsman Date: Mon, 1 Jun 2026 12:49:33 -0500 Subject: [PATCH 2/5] docs: fix architecture layers table in development guide Move audit from Layer 4 to Layer 1 (recording, not orchestration). Add orchestrator to Layer 4 where errgroup concurrency lives. Signed-off-by: Melvin Hillsman --- docs/development.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/development.md b/docs/development.md index 3407f7b..370e2a8 100644 --- a/docs/development.md +++ b/docs/development.md @@ -453,10 +453,10 @@ The codebase follows a strict layered architecture where each layer depends only | Layer | Packages | Goroutines | Purpose | |-------|----------|------------|---------| | 0 | `constants` | No | Pure values — API coordinates, labels, defaults | -| 1 | `config`, `cloudinit`, `cluster`, `logging` | No | Configuration, cloud-init YAML, K8s client init, structured logging | +| 1 | `config`, `cloudinit`, `cluster`, `logging`, `audit` | No | Configuration, cloud-init YAML, K8s client init, structured logging, audit tracking | | 2 | `vm`, `resources`, `wait` | Yes | K8s CRUD operations with retry, readiness polling | | 3 | `workloads` | No | Pure data producers — cloud-init specs, resource structs | -| 4 | `cmd/virtwork`, `cleanup`, `audit` | Yes | Orchestration, teardown, and audit tracking | +| 4 | `cmd/virtwork`, `orchestrator`, `cleanup` | Yes | Dependency wiring, orchestration, teardown | ### Concurrency Pattern From c45272d965c283ed000ffc12c88ced30250cd948 Mon Sep 17 00:00:00 2001 From: Melvin Hillsman Date: Mon, 1 Jun 2026 12:49:43 -0500 Subject: [PATCH 3/5] docs: fix DataVolumeTemplates and ServiceSpec signatures in override table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DataVolumeTemplates returns ([]DataVolumeTemplateSpec, error), not just nil. ServiceSpec takes no namespace parameter — the actual signature is ServiceSpec() *corev1.Service. Signed-off-by: Melvin Hillsman --- docs/development.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/development.md b/docs/development.md index 370e2a8..fc6f734 100644 --- a/docs/development.md +++ b/docs/development.md @@ -518,9 +518,9 @@ func (w *MyWorkload) CloudInitUserdata() (string, error) { |--------|---------|---------------| | `ExtraVolumes()` | `nil` | VM needs additional volume mounts | | `ExtraDisks()` | `nil` | VM needs additional disk definitions | -| `DataVolumeTemplates()` | `nil` | Workload needs persistent storage | +| `DataVolumeTemplates()` | `nil, nil` | Workload needs persistent storage (returns `[]DataVolumeTemplateSpec, error`) | | `RequiresService()` | `false` | VMs need a K8s Service for communication | -| `ServiceSpec(namespace)` | `nil` | Define the Service when `RequiresService()` is true | +| `ServiceSpec()` | `nil` | Define the Service when `RequiresService()` is true | | `VMCount()` | `1` | Workload needs multiple VMs (e.g., server/client) | For multi-VM workloads with per-role userdata, implement the `MultiVMWorkload` interface: From 1073b7ac1b1fe2f5a9e8b0eee7427a9b883d1e5e Mon Sep 17 00:00:00 2001 From: Melvin Hillsman Date: Mon, 1 Jun 2026 12:50:01 -0500 Subject: [PATCH 4/5] docs: update MultiVMWorkload interface and pattern to RoleDistribution Replace stale Roles() []string with RoleDistribution() []RoleSpec. Include RoleSpec type definition showing Role and VMCount fields. Rewrite pattern steps to explain per-role VM counts instead of len(Roles()) multiplication. Signed-off-by: Melvin Hillsman --- docs/development.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/development.md b/docs/development.md index fc6f734..5d1e639 100644 --- a/docs/development.md +++ b/docs/development.md @@ -572,17 +572,22 @@ If your workload needs more than one role of VM (e.g., a server and a client), i // In internal/workloads/workload.go type MultiVMWorkload interface { Workload - Roles() []string + RoleDistribution() []RoleSpec UserdataForRole(role string, namespace string) (string, error) } + +type RoleSpec struct { + Role string + VMCount int +} ``` Pattern: 1. Embed `BaseWorkload` and store any per-workload state (e.g., a `Namespace` field for in-cluster DNS). -2. Override `VMCount()` to return `count * len(Roles())` so the orchestrator creates the right number of VMs per role. -3. Override `Roles()` to return the role names (e.g., `[]string{"server", "client"}`). -4. Override `UserdataForRole(role, namespace)` to return role-specific cloud-init. The default `CloudInitUserdata()` typically delegates to `UserdataForRole("server", namespace)`. +2. Implement `RoleDistribution()` to return per-role VM counts (e.g., `[]RoleSpec{{Role: "server", VMCount: 1}, {Role: "client", VMCount: 1}}`). +3. Override `VMCount()` to sum the `RoleSpec.VMCount` values from `RoleDistribution()`. +4. Implement `UserdataForRole(role, namespace)` to return role-specific cloud-init. The default `CloudInitUserdata()` typically delegates to `UserdataForRole("server", namespace)`. 5. Set `RequiresService()` to `true` and provide a `ServiceSpec()` selecting server VMs by the `virtwork/role: server` label that the orchestrator applies automatically. 6. Clients reach servers via the in-cluster DNS name `..svc.cluster.local` — never poll for pod IPs. From 1b37e86440b424ae7cd0a522741108b304200256 Mon Sep 17 00:00:00 2001 From: Melvin Hillsman Date: Mon, 1 Jun 2026 12:50:16 -0500 Subject: [PATCH 5/5] docs: fix NamespaceDataVolumes path and concurrency narrative NamespaceDataVolumes is exported and lives in internal/orchestrator/types.go, not in cmd/virtwork/main.go. Concurrency narrative now correctly attributes errgroup usage to the orchestrator package. Signed-off-by: Melvin Hillsman --- docs/development.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/development.md b/docs/development.md index 5d1e639..c32fb56 100644 --- a/docs/development.md +++ b/docs/development.md @@ -460,7 +460,7 @@ The codebase follows a strict layered architecture where each layer depends only ### Concurrency Pattern -Go's native concurrency is used throughout. Parallel operations (VM creation, readiness polling, cleanup) use `errgroup.Group` for structured error handling, with `context.Context` for timeouts and cancellation. +Go's native concurrency is used throughout. The `internal/orchestrator` package drives parallel VM creation and readiness polling via `errgroup.Group` for structured error handling, with `context.Context` for timeouts and cancellation. ```go g, ctx := errgroup.WithContext(ctx) @@ -600,7 +600,7 @@ If your workload needs persistent storage inside the VM: 1. Override `DataVolumeTemplates()` to return a CDI `DataVolume` for each volume needed. Use `vm.BuildDataVolumeTemplate(name, size)` from `internal/vm`. 2. Override `ExtraVolumes()` and `ExtraDisks()` to wire the DataVolume into the VM. **Always set the `Serial` field on the `Disk`** — the in-VM script discovers the device through `/dev/disk/by-id/virtio-`, which is deterministic across reboots (unlike `/dev/vdX`, which is not). 3. In your cloud-init userdata, write the shared `diskSetupScript(serial, mountPoint)` helper (from `internal/workloads/workload.go`) as the first script. It waits for the symlink, formats with XFS if empty, mounts, and writes `/etc/fstab` for persistence across reboots. -4. The orchestrator's `namespaceDataVolumes` helper automatically suffixes DV template names with the VM name to avoid collisions across multiple VMs of the same workload. Your template name should be the un-suffixed base (e.g., `virtwork-chaos-disk-data`). +4. The `NamespaceDataVolumes` helper in `internal/orchestrator/types.go` automatically suffixes DV template names with the VM name to avoid collisions across multiple VMs of the same workload. Your template name should be the un-suffixed base (e.g., `virtwork-chaos-disk-data`). Reference workloads: `disk.go` (single fio mount), `database.go` (PostgreSQL data dir), `chaos_disk.go` (fill/release loop). All three follow the same pattern.