Docs
CRD Mode

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 ModeCRD Mode
Configure whereWeb consolekubectl apply or ArgoCD
Source of truthWave's internal storeGit + Custom Resources
Console roleFull read/writeRead-only for operational settings
Best forSingle cluster, quick iterationMulti-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

  1. You declare Wave configuration as Custom Resources in Git.
  2. ArgoCD (or kubectl) applies those CRs to the cluster.
  3. Wave watches the CRs and applies the same settings the console would — creating, updating, or reverting configuration as CRs are added, changed, or deleted.
  4. Each CR's .status reports whether Wave successfully reconciled it.

Two-layer validation:

  1. Structural errors — wrong types, unknown fields, missing required fields, invalid enum values — are caught at kubectl apply time by the CRD schema. The apply is rejected before Wave ever sees the resource.

  2. Cross-field semantic rules are evaluated by Wave after the apply succeeds. For example, a WaveAlertChannel must set exactly one of http, slackWebhook, or slackWebApi — setting none or more than one is a semantic error. Failures surface as ValidationFailed in .status.conditions and 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.yaml

See 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.

KindScopeConfiguresReference
WaveAutopilotPolicynamespacedAutopilot scaling policy for one workloadWave Autoscale CRDs
WaveAutopilotPresetclusterReusable scaling preset for schedulesWave Autoscale CRDs
WaveAutopilotScheduleclusterScheduled scaling windowsWave Autoscale CRDs
WaveSmartSizingPolicynamespacedSmart Sizing per-container bounds + auto-applyWave Sizing CRDs
WaveFlowPolicyclusterPriority load-shedding policyWave Flow CRDs
WaveNetfunnelMappingnamespacedWorkload ↔ NetFunnel project/segment mappingWave Flow CRDs
WaveNetfunnelConnectionclusterNetFunnel endpoint + credentialsWave Flow CRDs
WaveDiagnosisConfigclusterDiagnosis tasks (memory anomaly, PV forecast, PV cleanup)Wave Diagnosis CRDs
WaveKarpenterNodeWarmupclusterKarpenter NodePool warm-upWave Karpenter CRDs
WaveKarpenterSpotPlacementnamespacedSpot placement for one workloadWave Karpenter CRDs
WaveAlertChannelclusterAlert delivery channel (HTTP/Slack)Alerts CRDs
WaveAlertclusterAlert ruleAlerts CRDs
WavePvcAutoExpansionnamespacedPVC auto-expansion rulePV Lifecycle CRDs
WaveLabelGroupclusterLabel-based workload groupingWaveLabelGroup

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}'
ReasonSyncedMeaningWhat to do
AsExpectedTrueWave applied the CR successfully
RefInUseTrueA referenced resource is in use and was not removed
TargetNotFoundFalseThe target workload doesn't exist yetApply the workload or fix targetRef; self-heals
RefNotFoundFalseA referenced preset, channel, or similar doesn't exist yetApply the referenced CR; self-heals
SecretNotFoundFalseThe referenced Kubernetes Secret is missingCreate the Secret in wave-autoscale; self-heals
PartiallyAppliedFalseSome items in the CR applied, others didn'tCheck .status.message for detail; self-heals as conditions resolve
ConflictedFalseThe CR conflicts with another resource or an immutable fieldRead .status.message; manual fix required
ReconcileErrorFalseWave hit an unexpected errorRetry is automatic; check Wave logs if it persists
ValidationFailedFalseA 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 reasonArgoCD health
AsExpected, RefInUseHealthy
TargetNotFound, RefNotFound, SecretNotFound, PartiallyAppliedProgressing
ValidationFailed, Conflicted, ReconcileErrorDegraded

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 argocd

The 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
    - staging

Use 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: and

Spec fields

FieldTypeRequiredDefaultDescription
contextstring (enum)YesWhich feature uses this group. One of cluster_overview (node-label matching) or workload_report (workload-label + namespace matching). Immutable after creation.
descriptionstringNoHuman-readable description shown in the console.
labelFiltersobjectNo*Label-based filter conditions. Required unless namespaceFilters is set.
labelFilters.conditionsarrayYes (if labelFilters set)One or more label match conditions.
labelFilters.conditions[].keystringYesThe label key to match.
labelFilters.conditions[].operatorstringYesMatch operator: equals, in, or exists.
labelFilters.conditions[].valuestringNoSingle value for equals operator.
labelFilters.conditions[].valuesstring[]NoList of values for in operator.
labelFilters.logicstringYes (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.
namespaceFiltersstring[]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

  • context is immutable. Attempting to change it after creation is rejected by Wave — the CR reports Conflicted. 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 labelFilters nor namespaceFilters passes CRD schema validation but is rejected by Wave with ValidationFailed.
  • cluster_overview groups match node labels. namespaceFilters is ignored for this context because nodes have no namespace.
  • workload_report groups match workload labels and namespaces. Both labelFilters and namespaceFilters are evaluated together (AND logic between the two filter types).
  • Operator values are case-insensitive at reconcile time. Equals, equals, and EQUALS are equivalent. An unrecognized operator value causes ValidationFailed.