HorizontalPodAutoscaler Primitive¶
The hpa primitive wraps autoscaling/v2 HorizontalPodAutoscaler and integrates it with the component lifecycle as an
Operational, Graceful, and Suspendable resource.
Capabilities¶
The interfaces below are from pkg/component/concepts. The values in the table
are the runtime strings that appear in conditions.
| Interface | Reported status values | Notes |
|---|---|---|
Operational |
Operational, OperationPending, OperationFailing |
Inspects ScalingActive and AbleToScale |
Graceful |
Healthy, Degraded, Down |
Same HPA conditions, evaluated post-grace |
Suspendable |
PendingSuspension, Suspending, Suspended |
Delete-on-suspend by default |
Guardable |
Blocked |
Optional runtime precondition |
DataExtractable |
(side-effecting, no status) | Read generated fields after each sync cycle |
Building an HPA Primitive¶
import "github.com/sourcehawk/operator-component-framework/pkg/primitives/hpa"
base := &autoscalingv2.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: "backend-hpa",
Namespace: owner.Namespace,
},
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "backend",
},
MinReplicas: ptr.To(int32(2)),
MaxReplicas: 10,
},
}
resource, err := hpa.NewBuilder(base).
WithMutation(CPUScalingMutation(owner.Spec.Version)).
Build()
Mutations¶
Mutations are named functions that receive a *hpa.Mutator and record edit intent through typed editors. For a full
explanation of the mutation system, boolean-gated mutations, and version-gated mutations see
The Mutation System,
Boolean-Gated Mutations, and
Version-Gated Mutations.
A concise version-gated example:
var newScalingConstraint = semver.MustConstraint(">= 2.0.0")
func AggressiveScalingMutation(version string, enabled bool) hpa.Mutation {
return hpa.Mutation{
Name: "aggressive-scaling",
Feature: feature.NewVersionGate(version, []feature.VersionConstraint{newScalingConstraint}).
When(enabled),
Mutate: func(m *hpa.Mutator) error {
m.EditHPASpec(func(e *editors.HPASpecEditor) error {
e.SetMaxReplicas(20)
e.SetBehavior(&autoscalingv2.HorizontalPodAutoscalerBehavior{
ScaleDown: &autoscalingv2.HPAScalingRules{
StabilizationWindowSeconds: ptr.To(int32(60)),
},
})
return nil
})
return nil
},
}
}
Internal Mutation Ordering¶
Within a single mutation, edits execute in a fixed category order regardless of the order they are recorded:
| Step | Category | What it affects |
|---|---|---|
| 1 | Metadata edits | Labels and annotations on the HorizontalPodAutoscaler object |
| 2 | HPA spec edits | Scale target ref, min/max replicas, metrics, behavior |
Features apply in registration order. Later features observe the HPA as modified by all earlier ones.
Relevant Editors¶
For the full method list of any editor see the Go API reference. The generic concept is explained in Mutation Editors.
HPASpecEditor¶
Controls the HPA spec via m.EditHPASpec.
Available methods: SetScaleTargetRef, SetMinReplicas, SetMaxReplicas, EnsureMetric, RemoveMetric,
SetBehavior, Raw.
m.EditHPASpec(func(e *editors.HPASpecEditor) error {
e.SetMinReplicas(ptr.To(int32(2)))
e.SetMaxReplicas(10)
e.EnsureMetric(autoscalingv2.MetricSpec{
Type: autoscalingv2.ResourceMetricSourceType,
Resource: &autoscalingv2.ResourceMetricSource{
Name: corev1.ResourceCPU,
Target: autoscalingv2.MetricTarget{
Type: autoscalingv2.UtilizationMetricType,
AverageUtilization: ptr.To(int32(80)),
},
},
})
return nil
})
EnsureMetric identity rules¶
EnsureMetric upserts by full metric identity. If a matching entry exists it is replaced; otherwise the metric is
appended.
| Metric type | Match key |
|---|---|
Resource |
Resource.Name (e.g. cpu, memory) |
Pods |
Pods.Metric.Name + Pods.Metric.Selector (nil is a distinct identity) |
Object |
Object.DescribedObject (APIVersion, Kind, Name) + Object.Metric.Name + Object.Metric.Selector |
ContainerResource |
ContainerResource.Name + ContainerResource.Container |
External |
External.Metric.Name + External.Metric.Selector (nil is a distinct identity) |
RemoveMetric¶
RemoveMetric(type, name) removes all metrics matching the given type and name. For ContainerResource metrics all
container variants of the named resource are removed. For fine-grained removal of a single identity, use Raw() and
modify the slice directly.
SetBehavior¶
SetBehavior sets the autoscaling behavior (stabilization windows, scaling policies). Pass nil to remove custom
behavior and revert to Kubernetes defaults.
m.EditHPASpec(func(e *editors.HPASpecEditor) error {
e.SetBehavior(&autoscalingv2.HorizontalPodAutoscalerBehavior{
ScaleDown: &autoscalingv2.HPAScalingRules{
StabilizationWindowSeconds: ptr.To(int32(300)),
},
})
return nil
})
For fields not covered by the typed API, use Raw():
m.EditHPASpec(func(e *editors.HPASpecEditor) error {
e.Raw().MinReplicas = ptr.To(int32(1))
return nil
})
ObjectMetaEditor¶
Modifies labels and annotations via m.EditObjectMetadata.
Available methods: EnsureLabel, RemoveLabel, EnsureAnnotation, RemoveAnnotation, Raw.
m.EditObjectMetadata(func(e *editors.ObjectMetaEditor) error {
e.EnsureLabel("app.kubernetes.io/version", version)
return nil
})
Operational Status¶
The default handler inspects Status.Conditions:
| Status | Condition |
|---|---|
Operational |
ScalingActive is True |
OperationPending |
Conditions absent, or ScalingActive is Unknown |
OperationFailing |
ScalingActive is False, or AbleToScale is False |
AbleToScale = False takes precedence over ScalingActive = True because an HPA that cannot scale is not healthy
regardless of what the scaling-active condition reports.
Override with WithCustomOperationalStatus:
hpa.NewBuilder(base).
WithCustomOperationalStatus(func(op concepts.ConvergingOperation, h *autoscalingv2.HorizontalPodAutoscaler) (concepts.OperationalStatusWithReason, error) {
status, err := hpa.DefaultOperationalStatusHandler(op, h)
if err != nil {
return status, err
}
// Add custom logic
return status, nil
})
Grace Status¶
The default grace handler applies the same condition inspection after the grace period expires:
| Status | Condition |
|---|---|
Healthy |
ScalingActive is True |
Degraded |
Conditions absent, or ScalingActive is Unknown |
Down |
ScalingActive is False, or AbleToScale is False |
Override with WithCustomGraceStatus:
hpa.NewBuilder(base).
WithCustomGraceStatus(func(h *autoscalingv2.HorizontalPodAutoscaler) (concepts.GraceStatusWithReason, error) {
status, err := hpa.DefaultGraceStatusHandler(h)
if err != nil {
return status, err
}
// Add custom logic
return status, nil
})
Suspension¶
HPA has no native suspend field. The default behavior is delete on suspend: the HPA is removed when the component suspends and recreated on resume.
The reason this is necessary is the sequencing interaction with the HPA's scale target. When a Deployment (or other
workload) is suspended, the framework scales it to zero. A retained HPA would continuously enforce minReplicas and
scale the target back up, fighting the suspension. By deleting the HPA first, the target is free to scale down cleanly.
On resume the framework recreates the HPA before bringing the workload back.
The default suspension status handler reports Suspended immediately because the deletion is handled by the framework
and no additional convergence is required.
Override the deletion decision with WithCustomSuspendDeletionDecision:
hpa.NewBuilder(base).
WithCustomSuspendDeletionDecision(func(_ *autoscalingv2.HorizontalPodAutoscaler) bool {
return false // keep the HPA during suspension
})
When to keep the HPA
Retaining the HPA during suspension is only appropriate when the scale target is managed externally and will not be present during the component's suspension period. In the normal case where the HPA and its target are both managed by the same component, use the default delete behavior.
Override the suspension reason with WithCustomSuspendStatus if you need a message that reflects a non-default deletion
decision:
hpa.NewBuilder(base).
WithCustomSuspendStatus(func(_ *autoscalingv2.HorizontalPodAutoscaler) (concepts.SuspensionStatusWithReason, error) {
return concepts.SuspensionStatusWithReason{
Status: concepts.SuspensionStatusSuspended,
Reason: "HPA retained; scale target managed externally",
}, nil
})
Full Example¶
func AutoscalingMutation(version string) hpa.Mutation {
return hpa.Mutation{
Name: "autoscaling-config",
Feature: feature.NewVersionGate(version, nil),
Mutate: func(m *hpa.Mutator) error {
m.EditHPASpec(func(e *editors.HPASpecEditor) error {
e.SetMinReplicas(ptr.To(int32(2)))
e.SetMaxReplicas(10)
// CPU-based scaling target
e.EnsureMetric(autoscalingv2.MetricSpec{
Type: autoscalingv2.ResourceMetricSourceType,
Resource: &autoscalingv2.ResourceMetricSource{
Name: corev1.ResourceCPU,
Target: autoscalingv2.MetricTarget{
Type: autoscalingv2.UtilizationMetricType,
AverageUtilization: ptr.To(int32(70)),
},
},
})
// Memory-based scaling target
e.EnsureMetric(autoscalingv2.MetricSpec{
Type: autoscalingv2.ResourceMetricSourceType,
Resource: &autoscalingv2.ResourceMetricSource{
Name: corev1.ResourceMemory,
Target: autoscalingv2.MetricTarget{
Type: autoscalingv2.UtilizationMetricType,
AverageUtilization: ptr.To(int32(80)),
},
},
})
// Conservative scale-down to avoid thrashing
e.SetBehavior(&autoscalingv2.HorizontalPodAutoscalerBehavior{
ScaleDown: &autoscalingv2.HPAScalingRules{
StabilizationWindowSeconds: ptr.To(int32(300)),
},
})
return nil
})
m.EditObjectMetadata(func(e *editors.ObjectMetaEditor) error {
e.EnsureLabel("app.kubernetes.io/version", version)
return nil
})
return nil
},
}
}
resource, err := hpa.NewBuilder(base).
WithMutation(AutoscalingMutation(owner.Spec.Version)).
Build()
Although EditObjectMetadata is called after EditHPASpec in source, metadata edits are applied first per the internal
ordering. Call order inside Mutate is for readability only; the framework enforces the correct execution sequence.
Guidance¶
Feature: nil applies unconditionally. Omit Feature for mutations that always run. Use
feature.NewVersionGate(version, constraints) for version gating and chain .When(bool) for boolean conditions.
Register mutations in dependency order. If mutation B relies on a metric or field set by mutation A, register A first.
Use EnsureMetric for idempotent metric management. The editor matches by full metric identity so repeated calls
with the same identity update rather than duplicate.
Delete on suspend is the correct default. The HPA is removed during component suspension to prevent it from fighting a scale-to-zero workload. Only override the deletion decision when the scale target is managed externally.
Pair the suspension status handler with the deletion decision. The default suspension reason is intentionally
deletion-agnostic. If you override WithCustomSuspendDeletionDecision to retain the HPA, also override
WithCustomSuspendStatus so the reason accurately describes what is happening.