Introduction
A portable, provider-neutral Kubernetes API for virtual machines.
Source: https://github.com/vm-api/vm-api
VM API Architecture
VM API separates portable virtual machine configuration from infrastructure-specific implementation. Kubernetes users declare a VirtualMachine; a provider translates that intent into operations on its platform and publishes the resulting state.
Components
| Component | Responsibility |
|---|---|
| VirtualMachine API | Portable compute, storage, network, bootstrap, and power-state configuration |
| Generic controller | Provider-resource adoption, lifecycle coordination, and generic VM status |
| Provider status contract | Common fields and conditions read without provider-specific Go imports |
| Infrastructure provider | Platform-specific configuration, provisioning, observation, and cleanup |
The current API group and version are vm-api.io/v1alpha1.
Resource relationship
A generic VM’s spec.infrastructureRef identifies the API group, kind, and name of a provider resource in the same namespace. Kubernetes discovery supplies the preferred served version. The provider resource carries a vm-api.io/virtual-machine annotation naming the generic VM, allowing the controllers to verify the relationship before adoption.
The user or a higher-level controller creates both resources. The generic controller does not create provider resources or write their desired configuration. It establishes an owner reference, observes provider status, and coordinates deletion through a finalizer. The provider controller is responsible for applying desired configuration to the infrastructure and cleaning up its platform resources.
Provider boundary
The generic core reads a fixed set of status fields: provider identity, power state, addresses, provider metadata, and infrastructure readiness and update conditions. It does not call platform APIs or import provider types. Providers retain their own resource schemas and platform capabilities.
Portable fields describe shared intent. Provider-specific configuration remains on the provider resource. Image, network, storage, and sizing references must resolve in the destination environment; portability of configuration does not imply migration of running machines or their data.
Versioning and development
The API currently serves v1alpha1. Provider contract versioning, conformance, companion catalogs, and additional API versions are tracked in the roadmap. Future schema changes require explicit compatibility and upgrade guidance.
See Implementing a provider for integration requirements. The source and generated manifests in this repository describe the current implementation.
Implementing a VM API Provider
An infrastructure provider reconciles portable VirtualMachine intent against a specific platform. It owns the platform integration, exposes a Kubernetes resource for infrastructure-specific configuration, and publishes state through the shared provider contract.
This guide describes integration with the current vm-api.io/v1alpha1 API and generic controller. Provider implementations are separate from this repository.
Provider status contract
The core reads the following fields through an unstructured Kubernetes object. It does not require your provider’s Go types.
| Path | Meaning |
|---|---|
status.addresses[] | Network addresses, with interface, type, and address fields |
status.powerState | Observed platform power state |
status.providerID | Platform-specific machine identifier |
status.providerMetadata | Additional platform metadata as a string map |
status.conditions[] | Kubernetes conditions, including InfrastructureReady and UpToDate |
See contract.go for the field readers. Missing fields are allowed while a provider is converging. Publish conditions that describe infrastructure readiness and whether the requested configuration has been applied.
The core does not read or write the provider resource’s spec. It does modify ownership metadata and request deletion of the provider resource as part of VM lifecycle management. The provider controller must implement platform cleanup before allowing its own finalizer to be removed.
Link the resources
The user or a higher-level controller creates both the generic VM and the provider resource in the same namespace.
- Set the generic VM’s
spec.infrastructureRefto the provider resource’s API group, kind, and name. - Set the provider resource’s
vm-api.io/virtual-machineannotation to the generic VM’s name. - Before modifying platform resources, verify that both objects refer to each other.
The generic controller resolves the provider’s preferred served version using Kubernetes discovery. It checks the backlink before adoption and records a controller owner reference on the provider resource. The infrastructure reference is immutable after creation; see the VirtualMachine types.
A reference alone is not authorization to take control of an unrelated resource. Validate mutual linkage and namespace boundaries, and configure RBAC so only authorized users can establish the relationship. Handle missing or mismatched resources as explicit reconciliation states.
Resolve desired configuration
Read portable intent from the generic VM and resolve it to platform-specific configuration. Keep provider-only settings on the provider resource, with documented ownership and mutability rules.
A provider may persist resolved configuration into its own spec before acting on it. If using that pattern:
- Read supported portable fields such as power state, image, and instance sizing.
- Resolve references and validate platform constraints.
- Patch only changed provider-owned fields, using optimistic concurrency where needed.
- Reconcile the platform against the resolved configuration.
- Report observed state and conditions through the status contract.
Document which fields are controller-managed and which are user-managed. Do not overwrite provider-specific settings merely because they have no portable equivalent. An existing provider may use a different internal reconciliation model while satisfying the same external contract.
Resolve image and catalog references
Image and network references contain an API group, kind, and name. Define how your provider resolves those references and document the required catalog or platform resources. The core does not implement a universal image or network catalog.
The API validates reference names as DNS subdomains. Raw image strings containing registry paths, tags, or digests may therefore be invalid reference names. Use a supported catalog mapping or an explicitly designed provider-specific field rather than assuming any platform image identifier fits into a Kubernetes object reference.
Similarly, document how sizing profiles, failure domains, storage classes, and guest bootstrap data map to your platform. Report unsupported configurations clearly; do not silently ignore requested behavior.
Register controllers and schemes
The generic controller package exposes AddToManager. A provider can register it on the same controller-runtime manager as its provider controller.
Register both the generic API scheme and your provider’s scheme. Add a watch that maps changes to the generic VM back to its provider resource so desired-state updates reach your reconciler promptly.
Configure permissions for the generic VM, its status and finalizers, and the provider resource. The core needs to read provider status, patch ownership metadata, and delete provider resources. Review the checked-in RBAC markers against your actual provider group; do not assume permissions for an existing provider cover a new integration.
The current core uses timed reconciliation for provider states it does not watch directly. Account for asynchronous convergence in both the provider and its tests.
Platform lifecycle and recovery
Platform operations must tolerate retries and controller restarts. Choose idempotency keys and lookup behavior based on the platform’s guarantees. For asynchronous creation or eventually consistent lookups, distinguish a pending operation from a missing resource before creating a replacement.
Use a provider finalizer to complete platform cleanup when the provider object is deleted. The generic controller requests provider deletion and waits for the resource to disappear before completing generic VM deletion. Test failures and restarts throughout this sequence so a partially completed operation does not leak or duplicate infrastructure.
Testing
Keep unit tests independent of live platform credentials by placing platform calls behind interfaces and injecting test implementations. Use Kubernetes integration tests for object linkage, ownership, status updates, and finalizer behavior. Run separate end-to-end tests against the actual platform.
Cover at least:
- Successful creation, observation, power-state changes, and deletion.
- Missing, mismatched, or conflicting resource relationships.
- Unsupported configuration and failed reference resolution.
- Retried platform calls, asynchronous operations, and controller restarts.
- Concurrent resource updates and cleanup failures.
- Generic and provider status convergence, including readiness conditions.
Use eventual assertions for asynchronous behavior rather than fixed sleeps. The generic controller tests cover its adoption, status, and deletion behavior and provide a starting point for integration coverage.
Local integration workflow
- Install the generic CRD from config/crd/bases and your provider’s CRDs.
- Configure credentials and RBAC appropriate for the test environment.
- Run a manager with the generic and provider controllers registered.
- Create a mutually linked pair of generic and provider resources.
- Observe their status, change supported desired configuration, and verify cleanup on deletion.
If adoption does not complete, inspect the infrastructure reference, backlink annotation, namespace, served API version, ownership, and permissions. Adapt the sample VM to your provider’s resource kinds and catalog entries before applying it.
Testing VM API
API smoke test on kind
Run from the repository root:
make test-smoke
Prerequisites: Go with automatic toolchain downloads enabled, Python 3 and curl, and a running Docker daemon or Podman machine. Allow network access to download the pinned kind and kubectl tools and node image. The target installs kind and checksum-verified kubectl locally under the ignored tooling directory; it does not replace a system installation. The tested baseline is kind v0.33.0 with Kubernetes and kubectl v1.34.11, with the node image pinned by digest in the Makefile. This is one smoke-test baseline, not a supported Kubernetes version matrix.
The test creates a uniquely named, single-node cluster with a temporary kubeconfig. It installs the checked-in CRD, waits for its Established condition, verifies namespaced API discovery and resource verbs, and creates and reads a representative VirtualMachine. Assertions check persistence of image, network, sizing, and provider references, plus server-side power-state defaulting. It then deletes the VM and cluster. The normal kubeconfig and current context are untouched.
On failure, kind exports diagnostics to _artifacts/kind-smoke/ before cluster cleanup. CI uploads those logs with seven-day retention. Cluster cleanup runs on success, failure, and catchable interruption; a forcibly terminated process may require manual cleanup of the vm-api-smoke-* cluster identified in the logs.
The CI workflow runs this as smoke-kind on pull requests and main-branch pushes. It uses no provider credentials or infrastructure. No core or provider controllers are installed. Fixture references intentionally do not resolve to backing objects: this test checks Kubernetes API registration, admission, defaults, and storage, not reconciliation or VM provisioning.
The smoke test defaults to Docker, as used by CI. To use a running Podman machine locally:
KIND_EXPERIMENTAL_PROVIDER=podman make test-smoke
Override the image with make test-smoke KIND_NODE_IMAGE=<image> when investigating Kubernetes compatibility. Alternative node images require separate validation.
Controller integration tests
make -C controller test
These tests use envtest’s API server and etcd, run the generic controller, and exercise it against a stub provider resource. They cover adoption, status, and deletion coordination without creating infrastructure. They remain separate from the kind API smoke test.
Provider provisioning tests
Provisioning tests belong to each provider’s test suite. They must run separately from the credential-free API smoke test and demonstrate actual infrastructure creation, readiness, and deletion.
Before running one, provide:
- A dedicated Kubernetes test cluster with the VM API CRD, core controller, provider CRDs/controller, and required RBAC and webhook configuration.
- Provider infrastructure and scoped credentials. For vSphere this includes an accessible vCenter/test environment, compute capacity, VM images, networks, storage, and any required platform-specific services.
- Valid image, network, instance sizing, and storage references supported by that provider, plus any bootstrap Secrets.
- Quotas or spending limits, timeouts, ownership labels, and cleanup that removes provisioned resources even when a test fails.
- Provider/controller versions, infrastructure versions, and sanitized diagnostic collection sufficient to reproduce failures.
Credentials must stay in an approved secret store. Run credentialed tests only through a trusted, explicitly authorized workflow; never expose infrastructure credentials to unreviewed fork code. The provider test suite should document exact setup and teardown commands for its environment. This repository does not yet supply a provider provisioning test harness.