Custom workload kinds
6 minute read
StormForge optimizes Kubernetes workloads, and the Agent recognizes standard workload kinds by default — declare workloadResourceKinds to tell StormForge about custom resources or operators.
When to use
StormForge recognizes the built-in Kubernetes workload kinds (Deployment, StatefulSet, DaemonSet, ReplicaSet) automatically, while the default-enabled includeCommonAddOnResourceKinds flag adds support for common operators like Argo Rollouts, Prometheus operator, Dynatrace, and KEDA.
Configure custom workload kinds if:
- You run a custom workload resource that isn’t supported by default, such as the GitHub Actions Runner Controller (
AutoscalingRunnerSet) or the Spark operator (SparkApplication). - You run an operator that reconciles a standard workload and reverts direct patches — StormForge must patch the operator’s custom resource instead.
- You run an in-house platform operator that owns your workloads via
ownerReferenceswithout managing their resource fields.
Minimal configuration
For a custom workload resource that owns its own pods, add this to your Helm values:
workloadResourceKinds:
- apiGroup: example.com
kind: MyWorkload
resource: myworkloads
For other cases (operator-owned workloads, in-house platform operators, custom resources with nonstandard field locations), see Examples.
Workload model
StormForge’s workload model has three concepts:
- Pods — the workload’s replicas and the Agent’s starting point.
- Workload — the resource that owns the pods, visible in the UI.
- Patch target — where StormForge applies changes, usually the workload but can be an owner.
How StormForge identifies workloads
To identify a workload, the Agent walks up the pod’s ownerReferences chain to find the workload and the patch target. At each kind it encounters, it asks:
Is this the workload? The patch target? Do I keep climbing?
Declaring kinds in workloadResourceKinds and assigning each a role is how you answer those questions — the role determines how the Agent behaves when it encounters that kind during the climb.
Climbing requires Kubernetes RBAC, and StormForge only has RBAC for declared kinds. If the Agent encounters an undeclared kind, the climb stops with an RBAC failure.
Roles
| Role | Use when… | Example kinds |
|---|---|---|
workload (default) |
The kind is a scalable workload that owns pods. | Deployment, AutoscalingRunnerSet |
conditionalWorkload |
The kind acts as a workload when standalone but defers to a higher owner when one exists. Use for kinds like ReplicaSet that can be either. |
ReplicaSet, SparkApplication |
owner |
An operator or other link above the workload in the ownership chain. StormForge climbs through owners and patches the topmost one. | Prometheus, OpenTelemetryCollector |
ignoredOwner |
The kind owns workloads but doesn’t reconcile their resource fields. StormForge stops the climb below it and patches the workload directly. Note: if the operator does reconcile resources, those patches will silently fail. | in-house AppStack, PlatformBundle-style custom resources |
Role interactions
The top of the chain is the workload
The simplest case: the climb reaches the top, and the top is what gets patched. On the left: a StatefulSet declared workload. On the right: a ReplicaSet declared conditionalWorkload with nothing above it (so the conditional resolves to “act as workload”). Same outcome from different role declarations.
Adding an owner shifts the patch target up
On the left: a Deployment with no owner is the workload and the patch target. On the right: the same Pod → ReplicaSet → Deployment chain, with an OpenTelemetryCollector declared owner above the Deployment. The Deployment is still identified as the workload, but the climb continues past it and the patch lands on the owner — otherwise Deployment patches would be reverted on the next reconcile.
Two ways the climb terminates
On the left: ARC’s AutoscalingRunnerSet is declared workload, so the climb reaches it and patches there. On the right: Prometheus’s StatefulSet is the workload (built-in), Prometheus is the owner that gets patched, and the in-house AppStack above is declared ignoredOwner, so the climb stops below it.
Examples
Custom resource is a scalable workload
If your custom resource has a scale subresource and owns its pods, declare it as a workload. The default role is workload, so you can omit role::
workloadResourceKinds:
- apiGroup: example.com
kind: MyWorkload
resource: myworkloads
If the chain from pod to your custom resource goes through custom intermediate types (for example, pod → MyReplicaSet → MyWorkload), declare those as owner — the Agent will climb through them on its way to your workload:
- apiGroup: example.com
kind: MyReplicaSet
resource: myreplicasets
role: owner
Workload is owned by an operator that reverts patches
This is the Prometheus-operator pattern: a Prometheus custom resource owns a StatefulSet, and patching the StatefulSet directly gets reverted by the operator. Tell StormForge to patch the operator custom resource instead:
workloadResourceKinds:
- apiGroup: monitoring.coreos.com
kind: Prometheus
resource: prometheuses
role: owner
patchTargetDefaults:
containers.cpu.requests.patch-path: /spec/resources/requests/cpu
containers.memory.requests.patch-path: /spec/resources/requests/memory
containers.cpu.limits.patch-path: /spec/resources/limits/cpu
containers.memory.limits.patch-path: /spec/resources/limits/memory
Custom resource may be a workload or intermediate
The SparkApplication/ReplicaSet pattern: standalone, the resource is a workload; when owned by something higher (a ScheduledSparkApplication, a Deployment), it defers. Use conditionalWorkload:
- apiGroup: sparkoperator.k8s.io
kind: SparkApplication
resource: sparkapplications
role: conditionalWorkload
In-house operator owns workloads but doesn’t reconcile resources
Use this when a platform operator owns workloads but doesn’t manage their resource fields. Without configuration, StormForge climbs past the workloads to the operator custom resource, which isn’t a useful patch target. Declare the operator as ignoredOwner so the climb stops below it:
- apiGroup: platform.acme.io
kind: AppStack
resource: appstacks
role: ignoredOwner
Use ignoredOwner only when the operator doesn’t manage the workload’s resource fields — if it does, patches will silently fail.
Fields
| Field | Required | Description |
|---|---|---|
apiGroup |
Yes | API group (for example, actions.github.com). Use "" for core kinds. |
kind |
Yes | Kind, exactly as it appears in kubectl get (PascalCase). |
resource |
Yes | Plural resource name, lowercase — what kubectl api-resources shows and what RBAC rules use. |
role |
No | Which role the kind plays. Defaults to workload. |
patchTargetDefaults |
No | Where to write resource fields, when not at the standard pod-template location. |
role
Optional. Defaults to workload. See Roles for descriptions of each value.
patchTargetDefaults
Optional. Defines where StormForge writes resource fields when not at the standard pod-template location (/spec/template/spec/containers/.../resources).
Usually omitted for workload and conditionalWorkload entries; usually required on owner entries that are the topmost in their chain. Values are either JSON paths or Go templates. Settings here are defaults and can be overridden by per-workload annotations.
Example for an OpenTelemetryCollector:
- apiGroup: opentelemetry.io
kind: OpenTelemetryCollector
resource: opentelemetrycollectors
role: owner
patchTargetDefaults:
containers.cpu.requests.patch-path: /spec/resources/requests/cpu
containers.memory.requests.patch-path: /spec/resources/requests/memory
containers.cpu.limits.patch-path: /spec/resources/limits/cpu
containers.memory.limits.patch-path: /spec/resources/limits/memory
pod-template.metadata.patch-path: /spec/podAnnotations
pod-template.metadata.patch-format: '{ "stormforge.io/recommendation-url": "{{ .RecommendationURL }}" }'
pod-template.metadata.patch-path tells StormForge where to write annotations that trigger a rolling restart. pod-template.metadata.patch-format is a Go template for the annotation value.
For operator custom resources with resource fields in nonstandard locations (for example, Prometheus uses /spec/resources/...), patchTargetDefaults tells StormForge where to write. Intermediate owner entries that are never the topmost in their chain don’t need patchTargetDefaults — they’re climbed past, not patched.
Troubleshooting
StormForge isn’t optimizing my workload
- Is your workload resource declared with
role: workload? - If the pod is owned through intermediate custom resources, are those declared as
owner? StormForge climbs through declared owners — any undeclared link stops the climb.
My patches are being reverted
- If an operator owns the workload and reconciles it back, declare the operator with
role: ownerandpatchTargetDefaults. StormForge will then patch the operator’s custom resource.
StormForge patches an object that’s too high in the chain
- A broad owner is being treated as the workload. Declare it
ignoredOwnerso StormForge stops below it. If the owner does reconcile resource fields, userole: ownerinstead.
Permission errors in the Agent log about a custom resource
- The kind needs to be declared. Declaring it grants the Agent the RBAC it needs to read or patch it.