diff --git a/docs/guide/01-overview.md b/docs/guide/01-overview.md index 8dbbb86..91404b5 100644 --- a/docs/guide/01-overview.md +++ b/docs/guide/01-overview.md @@ -181,7 +181,7 @@ type Workload interface { VMResources() VMResourceSpec // "How many CPU cores and memory?" ExtraVolumes() []kubevirtv1.Volume // "Do you need extra volumes?" ExtraDisks() []kubevirtv1.Disk // "Do you need extra disks?" - DataVolumeTemplates() []kubevirtv1.DataVolumeTemplateSpec // "Do you need persistent storage?" + DataVolumeTemplates() ([]kubevirtv1.DataVolumeTemplateSpec, error) // "Do you need persistent storage?" RequiresService() bool // "Do you need a K8s Service?" ServiceSpec() *corev1.Service // "What should that Service look like?" VMCount() int // "How many VMs do you need?" @@ -219,12 +219,12 @@ Concrete workloads embed `BaseWorkload` and only override what they need. This c | **Disk** | `DataVolumeTemplates`, `ExtraDisks`, `ExtraVolumes` (virtio Serial + `diskSetupScript`) | Medium | | **Database** | `DataVolumeTemplates`, `ExtraDisks`, `ExtraVolumes` (virtio Serial + `diskSetupScript`) | Medium | | **Chaos-disk** | `DataVolumeTemplates`, `ExtraDisks`, `ExtraVolumes` (virtio Serial + `diskSetupScript`) | Medium | -| **Network** | `VMCount`, `Roles`, `UserdataForRole`, `RequiresService`, `ServiceSpec` (implements `MultiVMWorkload`) | Most complex | -| **TPS** | `VMCount`, `Roles`, `UserdataForRole`, `RequiresService`, `ServiceSpec`, `Params` (implements `MultiVMWorkload`) | Most complex | +| **Network** | `VMCount`, `RoleDistribution`, `UserdataForRole`, `RequiresService`, `ServiceSpec` (implements `MultiVMWorkload`) | Most complex | +| **TPS** | `VMCount`, `RoleDistribution`, `UserdataForRole`, `RequiresService`, `ServiceSpec`, `Params` (implements `MultiVMWorkload`) | Most complex | The multi-VM workloads (network, tps) are the most involved — they create two VMs per configured count (a server and a client), need a Kubernetes Service for DNS routing between them, and generate different cloud-init YAML for each role. -`MultiVMWorkload` extends `Workload` with `Roles()` and `UserdataForRole(role, namespace)`. The orchestrator type-asserts to this interface and dispatches per role; see [development.md](../development.md) for the implementation pattern. +`MultiVMWorkload` extends `Workload` with `RoleDistribution()` and `UserdataForRole(role, namespace)`. `RoleDistribution()` returns `[]RoleSpec`, where each `RoleSpec` declares a role name and how many VMs that role needs. The orchestrator type-asserts to this interface and iterates the distribution to create the correct number of VMs per role; see [development.md](../development.md) for the implementation pattern. ```mermaid flowchart LR @@ -272,7 +272,8 @@ If you want to dig deeper into how each component works: | How workloads define themselves | `internal/workloads/` — `Workload` and `MultiVMWorkload` interfaces in `workload.go`; implementations in `cpu.go`, `memory.go`, `disk.go`, `database.go`, `network.go`, `tps.go`, `chaos_disk.go`, `chaos_network.go`, `chaos_process.go` | | The `diskSetupScript` helper for storage-backed workloads | `internal/workloads/workload.go` — generates the wait/format/mount/fstab script for `/dev/disk/by-id/virtio-` | | How VMs are built from workload data | `internal/vm/vm.go` — `BuildVMSpec()` and `CreateVM()` | -| The CLI orchestration flow | `cmd/virtwork/main.go` — `runE()` and `cleanupE()`, including `namespaceDataVolumes` for per-VM DV naming | +| The run orchestration flow | `internal/orchestrator/orchestrator.go` — `RunOrchestrator.Run()` coordinates planning, resource creation, and readiness; `NamespaceDataVolumes` for per-VM DV naming is in `internal/orchestrator/types.go` | +| The CLI entry point | `cmd/virtwork/main.go` — `runE()` and `cleanupE()` wire dependencies and delegate to the orchestrator | | Configuration loading | `internal/config/config.go` — `LoadConfig()` with Viper | | Cloud-init YAML generation | `internal/cloudinit/cloudinit.go` — `BuildCloudConfig()` | | Resource helpers (namespace, service, secret) | `internal/resources/resources.go` | diff --git a/docs/guide/03-adding-a-workload.md b/docs/guide/03-adding-a-workload.md index e7b8339..505aa6e 100644 --- a/docs/guide/03-adding-a-workload.md +++ b/docs/guide/03-adding-a-workload.md @@ -15,7 +15,7 @@ This is a good first workload because it exercises the core `Workload` interface ## Before You Start -- Go 1.25+ installed +- Go 1.26+ installed - Ginkgo CLI installed: `go install github.com/onsi/ginkgo/v2/ginkgo@latest` - Read [How Virtwork Works](01-overview.md) to understand the workload interface - See [docs/development.md](../development.md) for environment setup @@ -303,29 +303,12 @@ All `HTTPWorkload` tests should pass, and existing workload tests remain green. ## Step 4: Register the Workload -The workload exists but the CLI doesn't know about it yet. Two changes are needed in `internal/workloads/registry.go`. - -### Add to AllWorkloadNames - -```go -// Before: -var AllWorkloadNames = []string{"cpu", "database", "disk", "memory", "network"} - -// After: -var AllWorkloadNames = []string{"cpu", "database", "disk", "http", "memory", "network"} -``` - -Keep the list alphabetically sorted. - -### Add the factory to DefaultRegistry +The workload exists but the CLI doesn't know about it yet. Add its factory to `DefaultRegistry()` in `internal/workloads/registry.go`: ```go func DefaultRegistry() Registry { return Registry{ - "cpu": func(cfg config.WorkloadConfig, opts *RegistryOpts) Workload { - return NewCPUWorkload(cfg, opts.SSHUser, opts.SSHPassword, opts.SSHAuthorizedKeys) - }, - // ... existing entries ... + // ... existing entries (chaos-disk, chaos-network, chaos-process, cpu, database, disk, memory, network, tps) ... "http": func(cfg config.WorkloadConfig, opts *RegistryOpts) Workload { return NewHTTPWorkload(cfg, opts.SSHUser, opts.SSHPassword, opts.SSHAuthorizedKeys) }, @@ -334,17 +317,19 @@ func DefaultRegistry() Registry { } ``` +That's the only change needed — `AllWorkloadNames()` is a function that derives its list from `DefaultRegistry().List()`, so adding the factory automatically includes `"http"` in the workload name list. + ### Update affected tests Adding a workload to the registry changes two things that existing tests verify: -1. **Registry tests** — The count of registered workloads increases from 5 to 6, and `List()` returns a different slice. +1. **Registry tests** — The count of registered workloads increases (currently 9 → 10), and `List()` returns a different slice. 2. **Orchestration tests** — If the default `--workloads` flag includes all workload names, the total VM count changes. Search for these assertions and update them: ```bash -grep -rn "AllWorkloadNames\|Len(5)\|HaveLen(5)" internal/ cmd/ +grep -rn "AllWorkloadNames\|Len(9)\|HaveLen(9)" internal/ cmd/ ``` Update any hard-coded counts to reflect the new workload. @@ -472,13 +457,13 @@ If your workload needs persistent storage (for example, a workload that writes b ```go // DataVolumeTemplates returns a CDI DataVolumeTemplateSpec. The orchestrator -// suffixes the template name with the VM name (namespaceDataVolumes in -// cmd/virtwork/main.go) to avoid collisions when --vm-count > 1, so use the -// un-suffixed base name here. -func (w *MyWorkload) DataVolumeTemplates() []kubevirtv1.DataVolumeTemplateSpec { +// suffixes the template name with the VM name (NamespaceDataVolumes in +// internal/orchestrator/types.go) to avoid collisions when --vm-count > 1, +// so use the un-suffixed base name here. +func (w *MyWorkload) DataVolumeTemplates() ([]kubevirtv1.DataVolumeTemplateSpec, error) { return []kubevirtv1.DataVolumeTemplateSpec{ vm.BuildDataVolumeTemplate("my-data", w.DataDiskSize), - } + }, nil } // ExtraDisks adds the disk definition to the VM spec. ALWAYS set the Serial @@ -536,13 +521,13 @@ return w.BuildCloudConfig(CloudConfigOpts{ If your workload needs more than one role of VM (a server and one or more clients, for example), implement the `MultiVMWorkload` interface. The two canonical references are `internal/workloads/network.go` (simplest — one Service port, iperf3) and `internal/workloads/tps.go` (multi-port Service with configurable `Params` for `file-size`, `iterations`, `duration`). 1. Add a `Namespace` field to your struct — the client needs it to build the server's in-cluster DNS name. -2. Implement `Roles() []string` (e.g., `[]string{"server", "client"}`). +2. Implement `RoleDistribution() []RoleSpec` — return a slice of `RoleSpec{Role: "server", VMCount: 1}` entries declaring how many VMs each role needs. 3. Implement `UserdataForRole(role, namespace) (string, error)` — return different cloud-init YAML per role. The orchestrator dispatches per role; the client constructs `..svc.cluster.local` and never polls for pod IPs. -4. Override `VMCount()` to return `count * len(Roles())` so the orchestrator creates the right number of VMs. +4. Override `VMCount()` to return the sum of all `RoleSpec.VMCount` values from `RoleDistribution()`. 5. Override `RequiresService()` to return `true`. 6. Implement `ServiceSpec()` to create a ClusterIP Service. Its selector should match `virtwork/role: server` (and ideally `app.kubernetes.io/component: `) — the orchestrator applies the `virtwork/role` label to each VM automatically. -The orchestrator detects `MultiVMWorkload` via type assertion and calls `UserdataForRole()` per role/instance instead of `CloudInitUserdata()`. +The orchestrator detects `MultiVMWorkload` via type assertion, iterates `RoleDistribution()`, and calls `UserdataForRole()` for each role/instance instead of `CloudInitUserdata()`. ### Workload Complexity Spectrum @@ -550,7 +535,7 @@ The orchestrator detects `MultiVMWorkload` via type assertion and calls `Userdat flowchart LR A["Simple
CPU, Memory, Chaos-process
Name + CloudInit only"] B["With Storage
Disk, Database, Chaos-disk
+ DataVolumeTemplates
+ ExtraDisks (with Serial)
+ ExtraVolumes
+ diskSetupScript
"] - C["Multi-VM
Network, TPS
+ MultiVMWorkload
(Roles, UserdataForRole)
+ Service + VMCount
"] + C["Multi-VM
Network, TPS
+ MultiVMWorkload
(RoleDistribution, UserdataForRole)
+ Service + VMCount
"] A --> B --> C ``` @@ -575,9 +560,9 @@ Before submitting a new workload, verify: **If multi-VM:** -- [ ] Implements `Roles() []string` +- [ ] Implements `RoleDistribution() []RoleSpec` - [ ] Implements `UserdataForRole(role, namespace) (string, error)` -- [ ] `VMCount()` returns `Config.VMCount * len(Roles())` +- [ ] `VMCount()` returns the sum of all `RoleSpec.VMCount` values from `RoleDistribution()` - [ ] `ServiceSpec().Spec.Selector` includes `virtwork/role: ` and `app.kubernetes.io/component: ` - [ ] Client userdata builds the server DNS as `..svc.cluster.local` diff --git a/docs/guide/README.md b/docs/guide/README.md index 35689d0..4ab6584 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -11,7 +11,7 @@ This guide walks you through how virtwork works, how to deploy workloads with it - Familiarity with Kubernetes/OpenShift concepts (namespaces, pods, services) - For the deployment demo: an OpenShift cluster with [OpenShift Virtualization](https://docs.openshift.com/container-platform/latest/virt/about_virt/about-virt.html) (CNV) installed -- For the developer tutorial: Go 1.25+ and the [Ginkgo CLI](https://onsi.github.io/ginkgo/#installing-ginkgo) +- For the developer tutorial: Go 1.26+ and the [Ginkgo CLI](https://onsi.github.io/ginkgo/#installing-ginkgo) ## Guide Sections