CRD Mode
Wave runs in one of two operation modes.
Console Mode (default) — every operational setting is configured through the web console and stored by Wave. This is where most teams start.
CRD Mode — operational configuration is declared as Custom Resources (wavek8s.com/v1alpha1) in Git and applied to the cluster via ArgoCD or kubectl. Wave watches those resources and applies the same settings the console would. The web console becomes read-only for operational settings.
Reasons to adopt CRD Mode:
- Same configuration across many clusters — one Git repo, identical behavior everywhere.
- PR-reviewed config changes — every policy update goes through your normal review workflow.
- No console drift — the cluster state is always derivable from the repo; no undocumented console-only changes.
Console Mode vs CRD Mode
| Console Mode | CRD Mode | |
|---|---|---|
| Configure where | Web console | kubectl apply or ArgoCD |
| Source of truth | Wave's internal store | Git + Custom Resources |
| Console role | Full read/write | Read-only for operational settings |
| Best for | Single cluster, quick iteration | Multi-cluster, GitOps workflows, regulated environments |
In CRD Mode, the following items remain editable in the console and are not managed by CRDs:
- User accounts and roles
- SSO configuration
- License management
Everything else — scaling policies, alert rules, sizing bounds, flow policies, diagnosis tasks — is owned by your CRs.
How it works
- You declare Wave configuration as Custom Resources in Git.
- ArgoCD (or
kubectl) applies those CRs to the cluster. - Wave watches the CRs and applies the same settings the console would — creating, updating, or reverting configuration as CRs are added, changed, or deleted.
- Each CR's
.statusreports whether Wave successfully reconciled it.
Two-layer validation:
-
Structural errors — wrong types, unknown fields, missing required fields, invalid enum values — are caught at
kubectl applytime by the CRD schema. The apply is rejected before Wave ever sees the resource. -
Cross-field semantic rules are evaluated by Wave after the apply succeeds. For example, a
WaveAlertChannelmust set exactly one ofhttp,slackWebhook, orslackWebApi— setting none or more than one is a semantic error. Failures surface asValidationFailedin.status.conditionsand require a spec fix and reapply.
Separately, a CR whose target or reference doesn't exist yet is not a validation failure — it reports TargetNotFound or RefNotFound in .status and converges automatically when the target appears.
Throughout the reference pages, the Default column documents the value Wave effectively uses when a field is omitted — some of these are applied by Wave at reconcile time rather than encoded as defaults in the CRD schema itself. Likewise, a few Required markings are enforced by Wave at reconcile time rather than by the API server — the affected field descriptions say so.
Enabling CRD Mode
Before enabling, review the warnings below. There is no automatic export from Console Mode — anything not declared in Git at flip time reverts to its default.
Write your CRs to Git and verify they apply cleanly before flipping WA_CONFIG_MODE. At the moment Wave restarts in CRD Mode, any setting not backed by a CR reverts to its default. See Switching modes.
Step 1 — CRDs are installed by default.
The Helm chart installs the 14 wavek8s.com CRDs automatically (crds.install: true). Running helm upgrade refreshes the CRD schemas. Running helm uninstall leaves the CRDs and your CRs in place (each CRD carries helm.sh/resource-policy: keep), so your configuration survives a chart removal.
If your team manages CRDs out-of-band — for example, a cluster admin pre-installs them, or ArgoCD applies them separately — set crds.install: false in your values file to prevent the chart from touching them.
Step 2 — Set WA_CONFIG_MODE to crd and upgrade.
Add the environment variable to the existing spec.core.env list in your values file. The list ships with required defaults (WA_API_SERVER_HOST, WA_LICENSE, and others) — add the new entry rather than replacing the list.
spec:
core:
env:
# ...keep the existing entries...
- name: WA_CONFIG_MODE
value: "crd"Then apply:
helm upgrade wave-autoscale wave-autoscale/wave-autoscale \
-n wave-autoscale \
-f wa-values.yamlSee the base installation guide for the full helm upgrade command and required values.
Step 3 — RBAC is automatic.
The chart grants Wave read access to its own CRs and write access to their .status subresources. Nothing extra to configure.
The 14 kinds
All kinds are under apiVersion: wavek8s.com/v1alpha1.
| Kind | Scope | Configures | Reference |
|---|---|---|---|
WaveAutopilotPolicy | namespaced | Autopilot scaling policy for one workload | Wave Autoscale CRDs |
WaveAutopilotPreset | cluster | Reusable scaling preset for schedules | Wave Autoscale CRDs |
WaveAutopilotSchedule | cluster | Scheduled scaling windows | Wave Autoscale CRDs |
WaveSmartSizingPolicy | namespaced | Smart Sizing per-container bounds + auto-apply | Wave Sizing CRDs |
WaveFlowPolicy | cluster | Priority load-shedding policy | Wave Flow CRDs |
WaveNetfunnelMapping | namespaced | Workload ↔ NetFunnel project/segment mapping | Wave Flow CRDs |
WaveNetfunnelConnection | cluster | NetFunnel endpoint + credentials | Wave Flow CRDs |
WaveDiagnosisConfig | cluster | Diagnosis tasks (memory anomaly, PV forecast, PV cleanup) | Wave Diagnosis CRDs |
WaveKarpenterNodeWarmup | cluster | Karpenter NodePool warm-up | Wave Karpenter CRDs |
WaveKarpenterSpotPlacement | namespaced | Spot placement for one workload | Wave Karpenter CRDs |
WaveAlertChannel | cluster | Alert delivery channel (HTTP/Slack) | Alerts CRDs |
WaveAlert | cluster | Alert rule | Alerts CRDs |
WavePvcAutoExpansion | namespaced | PVC auto-expansion rule | PV Lifecycle CRDs |
WaveLabelGroup | cluster | Label-based workload grouping | WaveLabelGroup |
Core behaviors
Declarative ownership and pruning. A setting exists because a CR declares it. Delete the CR and Wave reverts the setting to its default on the next reconcile. Wave watches its CRs, so the revert happens within a few seconds of the delete.
Exception — WaveNetfunnelConnection: deleting the CR disables the connection rather than erasing it. See Wave Flow CRDs.
Portable name-based references. CRs reference each other by targetRef.kind + targetRef.name, or by plain name (for channels, presets, and similar). No internal IDs are exposed, so the same YAML applies identically to every cluster.
Namespacing. Workload-scoped kinds (WaveAutopilotPolicy, WaveSmartSizingPolicy, WaveNetfunnelMapping, WaveKarpenterSpotPlacement, WavePvcAutoExpansion) are namespaced and must be created in the same namespace as their target workload. Global kinds are cluster-scoped.
Secrets. Only WaveAlertChannel and WaveNetfunnelConnection carry credentials. Both reference a Kubernetes Secret in the wave-autoscale namespace by {name, key} — Wave never stores credentials in the CR itself.
Apply order doesn't matter. A CR that references a not-yet-existing target succeeds at kubectl apply time. Wave reports TargetNotFound or RefNotFound in .status and reconciles automatically when the target appears. You can apply an entire directory of CRs in one shot without worrying about ordering.
Sync status and health
Every kind carries a single Synced condition on .status.conditions. Replace the resource name with your kind's lowercase plural — kubectl api-resources | grep wavek8s lists them all. Check it with:
kubectl get waveautopilotpolicies -n payment my-policy \
-o jsonpath='{.status.conditions[?(@.type=="Synced")].reason}'| Reason | Synced | Meaning | What to do |
|---|---|---|---|
AsExpected | True | Wave applied the CR successfully | — |
RefInUse | True | A referenced resource is in use and was not removed | — |
TargetNotFound | False | The target workload doesn't exist yet | Apply the workload or fix targetRef; self-heals |
RefNotFound | False | A referenced preset, channel, or similar doesn't exist yet | Apply the referenced CR; self-heals |
SecretNotFound | False | The referenced Kubernetes Secret is missing | Create the Secret in wave-autoscale; self-heals |
PartiallyApplied | False | Some items in the CR applied, others didn't | Check .status.message for detail; self-heals as conditions resolve |
Conflicted | False | The CR conflicts with another resource or an immutable field | Read .status.message; manual fix required |
ReconcileError | False | Wave hit an unexpected error | Retry is automatic; check Wave logs if it persists |
ValidationFailed | False | A semantic rule failed (cross-field validation) | Fix the CR spec and reapply |
The web console surfaces CRD Mode in three places: a CRD Mode indicator in the sidebar, an Admin → CRD Status page showing every CR with its state and reason, and a reconciler health card on the Admin → CRD Status page.
ArgoCD integration
By default, ArgoCD marks unknown custom resources as Healthy. Wave ships a health check that maps each CR's .status to proper ArgoCD health states:
.status reason | ArgoCD health |
|---|---|
AsExpected, RefInUse | Healthy |
TargetNotFound, RefNotFound, SecretNotFound, PartiallyApplied | Progressing |
ValidationFailed, Conflicted, ReconcileError | Degraded |
Setup: Wave's Helm chart renders a ConfigMap named argocd-resource-customizations-example in the wave-autoscale namespace containing the health check Lua scripts. Extract it, review it, then merge its resource.customizations entries into the argocd-cm ConfigMap in your argocd namespace:
# Extract the example ConfigMap
kubectl get cm argocd-resource-customizations-example \
-n wave-autoscale -o yaml
# Merge its entries into argocd-cm in your argocd namespace
kubectl edit cm argocd-cm -n argocdThe ConfigMap's data keys follow the format resource.customizations.health.wavek8s.com_<Kind> (for example, resource.customizations.health.wavek8s.com_WaveAutopilotPolicy); copy those keys under the same paths in argocd-cm. See the ArgoCD custom health check format (opens in a new tab) for the full specification. Note that a malformed argocd-cm affects all ArgoCD syncs cluster-wide — review the diff carefully before applying.
Once merged, ArgoCD reflects the true reconcile state of every Wave CR in its UI and sync status immediately — no ArgoCD restart needed.
Switching modes
Console Mode → CRD Mode: there is no automatic export. At the moment Wave restarts in CRD Mode, any operational setting not backed by a CR in the cluster reverts to its default. Write and apply your CRs first — or adopt CRD Mode at install time before configuring anything in the console.
CRD Mode → Console Mode: safe. Set WA_CONFIG_MODE back to console (or remove the env var) and run helm upgrade. The last synced configuration becomes the editable baseline in the console immediately — no data loss. The CRs remain in the cluster but Wave ignores them from that point on; they are not deleted.
WaveLabelGroup
Purpose: WaveLabelGroup defines a named group of workloads or nodes based on label and namespace filters. Groups are used by cluster overview filtering and workload reports to slice metrics across a meaningful subset of your cluster.
Scope: cluster-scoped (no metadata.namespace required).
Example
apiVersion: wavek8s.com/v1alpha1
kind: WaveLabelGroup
metadata:
name: prod-backend-label-group
spec:
context: workload_report
description: "Production backend services across prod and staging namespaces."
labelFilters:
conditions:
- key: app.kubernetes.io/component
operator: in
values:
- backend
- api
- key: app.kubernetes.io/managed-by
operator: exists
logic: and
namespaceFilters:
- prod
- stagingUse cluster_overview when you want to filter by node labels — for example, grouping Spot instances or nodes in a specific availability zone for cluster-level dashboards. Use workload_report when you want to slice metrics by workload labels and namespaces. For a cluster_overview group (matching node labels):
apiVersion: wavek8s.com/v1alpha1
kind: WaveLabelGroup
metadata:
name: spot-nodes-label-group
spec:
context: cluster_overview
description: "EKS Spot m5 nodes with an availability-zone label."
labelFilters:
conditions:
- key: eks.amazonaws.com/capacityType
operator: equals
value: SPOT
- key: node.kubernetes.io/instance-type
operator: in
values:
- m5.large
- m5.xlarge
- key: topology.kubernetes.io/zone
operator: exists
logic: andSpec fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
context | string (enum) | Yes | — | Which feature uses this group. One of cluster_overview (node-label matching) or workload_report (workload-label + namespace matching). Immutable after creation. |
description | string | No | — | Human-readable description shown in the console. |
labelFilters | object | No* | — | Label-based filter conditions. Required unless namespaceFilters is set. |
labelFilters.conditions | array | Yes (if labelFilters set) | — | One or more label match conditions. |
labelFilters.conditions[].key | string | Yes | — | The label key to match. |
labelFilters.conditions[].operator | string | Yes | — | Match operator: equals, in, or exists. |
labelFilters.conditions[].value | string | No | — | Single value for equals operator. |
labelFilters.conditions[].values | string[] | No | — | List of values for in operator. |
labelFilters.logic | string | Yes (runtime) | "" | Combinator for multiple conditions: and or or. The schema injects an empty string when omitted, which Wave rejects with ValidationFailed — always set it explicitly. |
namespaceFilters | string[] | No* | — | List of namespaces to include. Evaluated alongside labelFilters for workload_report. Ignored for cluster_overview (nodes have no namespace). Required unless labelFilters is set. |
*At least one of labelFilters or namespaceFilters must be provided.
Notes
contextis immutable. Attempting to change it after creation is rejected by Wave — the CR reportsConflicted. To change the context, delete the CR and create a new one with the desired value.- At least one filter is required at runtime. A CR with neither
labelFiltersnornamespaceFilterspasses CRD schema validation but is rejected by Wave withValidationFailed. cluster_overviewgroups match node labels.namespaceFiltersis ignored for this context because nodes have no namespace.workload_reportgroups match workload labels and namespaces. BothlabelFiltersandnamespaceFiltersare evaluated together (AND logic between the two filter types).- Operator values are case-insensitive at reconcile time.
Equals,equals, andEQUALSare equivalent. An unrecognized operator value causesValidationFailed.