Skip to content

Commit 16ee442

Browse files
authored
feat(kubernetes): add the autoscaler, policy, networking and Karpenter kinds (#91)
1 parent 1dd98a0 commit 16ee442

14 files changed

Lines changed: 1410 additions & 5 deletions
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Package autoscaling models the Kubernetes autoscalers as a service in the
2+
// tree. Unlike the apps, batch and core groups, its members provision nothing
3+
// and cost nothing directly — they are held because they decide whether a
4+
// change to a workload's manifest has any effect.
5+
//
6+
// A rightsizing recommendation edits the requests and limits declared on a
7+
// workload. An autoscaler attached to that workload can overwrite those values
8+
// at admission (VerticalPodAutoscaler) or make the declared replica count dead
9+
// config (HorizontalPodAutoscaler). Either way a pull request that edits the
10+
// manifest changes a number the cluster ignores, and the finding never
11+
// resolves. That is a correctness problem rather than a coverage one, which is
12+
// why these are in the tree despite carrying no cost of their own.
13+
//
14+
// The two kinds are separate Kubernetes API groups — HorizontalPodAutoscaler is
15+
// autoscaling/v2, built in; VerticalPodAutoscaler is autoscaling.k8s.io/v1, a
16+
// CRD installed with the VPA controller. They share this package because they
17+
// are the same concern from the tree's point of view: something other than the
18+
// manifest is deciding a workload's resources.
19+
//
20+
// Each slice is tagged with the kind, which becomes the resource Type on the
21+
// wire — mirroring the apps, batch and core groups.
22+
package autoscaling
23+
24+
import (
25+
"github.com/infracost/go-proto/pkg/tree/value"
26+
)
27+
28+
// Autoscaling is the group holding the Kubernetes autoscaler kinds.
29+
type Autoscaling struct {
30+
VerticalPodAutoscalers []VerticalPodAutoscaler `tree:"verticalpodautoscaler"`
31+
HorizontalPodAutoscalers []HorizontalPodAutoscaler `tree:"horizontalpodautoscaler"`
32+
}
33+
34+
// TargetRef identifies the workload an autoscaler acts on — spec.targetRef on a
35+
// VerticalPodAutoscaler, spec.scaleTargetRef on a HorizontalPodAutoscaler.
36+
//
37+
// This is the join back to the workload, and it is a pointer rather than an
38+
// identity: the autoscaler names the workload, so finding the autoscaler that
39+
// governs a given Deployment means searching by these fields rather than by the
40+
// autoscaler's own name, which is arbitrary and frequently unrelated.
41+
//
42+
// Kind and Name are namespace-scoped — an autoscaler can only target a workload
43+
// in its own namespace — so the namespace on the embedding kind's ObjectMeta
44+
// completes the reference.
45+
type TargetRef struct {
46+
// APIVersion is the target's apiVersion, e.g. "apps/v1". Optional in both
47+
// CRDs and frequently omitted, so an empty value means unspecified rather
48+
// than absent.
49+
APIVersion value.String `tree:"api_version"`
50+
51+
// Kind is the target's kind, e.g. "Deployment" or "StatefulSet". Recorded
52+
// verbatim from the manifest, so it keeps the CamelCase the Kubernetes API
53+
// uses rather than the lower-cased form the tree's addresses carry.
54+
Kind value.String `tree:"kind"`
55+
56+
// Name is the target workload's metadata.name.
57+
Name value.String `tree:"name"`
58+
}
59+
60+
// ResourceAmounts is a CPU/memory pair as an autoscaler or a LimitRange states
61+
// it, in the same base units the workload containers use — CPU in millicores,
62+
// memory in bytes — so a bound can be compared against a container's request
63+
// without converting first.
64+
//
65+
// Both are optional in every position they appear: a policy may bound only CPU,
66+
// or only memory. Note that absence is not representable on its own — an unset
67+
// value serializes as a zero — and a zero bound reads as "pinned to nothing"
68+
// rather than "unbounded". Where the distinction matters the parser marks the
69+
// value it filled in with flag.Synthetic, which does survive, the same way
70+
// meta.ObjectMeta.Namespace records an assumed namespace.
71+
type ResourceAmounts struct {
72+
CPUMillicores value.Int `tree:"cpu_millicores"`
73+
MemoryBytes value.Int `tree:"memory_bytes"`
74+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package autoscaling
2+
3+
import (
4+
"github.com/infracost/go-proto/pkg/tree/kubernetes/meta"
5+
"github.com/infracost/go-proto/pkg/tree/resource"
6+
"github.com/infracost/go-proto/pkg/tree/value"
7+
)
8+
9+
// MetricSourceType values for a HorizontalPodAutoscaler's spec.metrics[].type.
10+
// The distinction that matters is whether the metric is a share of what the
11+
// container requests: only Resource and ContainerResource are, and only those
12+
// couple the replica count to the numbers a rightsizing recommendation edits.
13+
const (
14+
// MetricSourceTypeResource scales on a resource the pod requests — cpu or
15+
// memory — summed across the pod's containers.
16+
MetricSourceTypeResource = "Resource"
17+
18+
// MetricSourceTypeContainerResource is the same, narrowed to one named
19+
// container rather than the pod total. On a multi-container pod this says
20+
// which container's request the replica count actually keys off.
21+
MetricSourceTypeContainerResource = "ContainerResource"
22+
23+
// MetricSourceTypePods scales on a custom per-pod metric averaged over the
24+
// pods. Not a share of anything requested, so it does not couple to the
25+
// container's resources.
26+
MetricSourceTypePods = "Pods"
27+
28+
// MetricSourceTypeObject scales on a metric describing some other
29+
// Kubernetes object. The described object is not modelled.
30+
MetricSourceTypeObject = "Object"
31+
32+
// MetricSourceTypeExternal scales on a metric from outside the cluster — a
33+
// queue depth, a request rate. The replica count is then driven by
34+
// something no manifest describes.
35+
MetricSourceTypeExternal = "External"
36+
)
37+
38+
// MetricTargetType values for a metric's target.type — which of the target's
39+
// value fields the manifest set.
40+
const (
41+
// MetricTargetTypeUtilization targets a percentage of the requested
42+
// resource. This is the setpoint that makes observed headroom expected
43+
// rather than wasted, and it is only valid on Resource and
44+
// ContainerResource metrics.
45+
MetricTargetTypeUtilization = "Utilization"
46+
47+
// MetricTargetTypeValue targets a raw metric value.
48+
MetricTargetTypeValue = "Value"
49+
50+
// MetricTargetTypeAverageValue targets a raw metric value averaged over the
51+
// pods.
52+
MetricTargetTypeAverageValue = "AverageValue"
53+
)
54+
55+
// HorizontalPodAutoscaler is an autoscaling/v2 HorizontalPodAutoscaler.
56+
//
57+
// It provisions nothing and costs nothing, and unlike a VerticalPodAutoscaler
58+
// it does not touch container requests — so it does not invalidate a rightsizing
59+
// recommendation. What it invalidates is the workload's declared replica count:
60+
// once an HPA governs a Deployment, spec.replicas in the manifest is read at
61+
// creation and then never again, and a recommendation that proposes editing it
62+
// is proposing a change with no effect.
63+
//
64+
// It also changes how a per-pod saving becomes a real one. Shrinking a request
65+
// on an HPA-governed workload does not reduce the pod count, it makes each pod
66+
// cheaper to schedule and lets the same node fit more of them — so the saving
67+
// only banks if the node count follows, which is the node-coupling question
68+
// rather than the pod one.
69+
//
70+
// MinReplicas and MaxReplicas are held for that reason: they bound how much of
71+
// the estate the workload can occupy, which is what a saving is computed
72+
// against.
73+
//
74+
// Metrics are held for a sharper one. A utilization target is a setpoint rather
75+
// than an observation: an HPA holding a Deployment at 50% CPU produces a
76+
// workload sitting at 50% of its request by design, and a rightsizing pass that
77+
// reads that as half wasted will propose halving the request. Halving it puts
78+
// utilization back at the target, the HPA scales out, and the same spend
79+
// returns as more smaller pods. The observed utilization is something the
80+
// metrics pipeline reports and reports better; the target it is being held at
81+
// exists only in the manifest, the same way a VerticalPodAutoscaler's
82+
// updateMode does.
83+
//
84+
// The kind, address ([namespace, kind, name]) and source range live on the
85+
// embedded resource.Resource; the HPA's own name and namespace on the embedded
86+
// meta.ObjectMeta; and its Kubernetes labels are stored as the base resource's
87+
// Tags.
88+
type HorizontalPodAutoscaler struct {
89+
resource.Resource `tree:"-"`
90+
meta.ObjectMeta `tree:"-"`
91+
92+
// ScaleTargetRef is spec.scaleTargetRef — the workload this HPA scales.
93+
// Required by the API, so an empty value means a malformed manifest.
94+
ScaleTargetRef TargetRef `tree:"scale_target_ref"`
95+
96+
// MinReplicas is spec.minReplicas. Optional, defaulting to 1 when omitted —
97+
// so unset is not zero, and reading it as zero would suggest the workload
98+
// can scale to nothing, which it cannot without a separate feature gate.
99+
MinReplicas value.Int `tree:"min_replicas"`
100+
101+
// MaxReplicas is spec.maxReplicas, required by the API. This is the ceiling
102+
// a worst-case cost is computed against.
103+
MaxReplicas value.Int `tree:"max_replicas"`
104+
105+
// Metrics are spec.metrics — what the controller scales on, and the value
106+
// it holds that signal at. Empty when the manifest states none, in which
107+
// case the controller falls back to a default CPU utilization target that
108+
// is cluster configuration rather than repository state.
109+
Metrics []Metric `tree:"metrics"`
110+
111+
// Annotations are the HPA's Kubernetes annotations, surfaced verbatim.
112+
Annotations []resource.Tag `tree:"annotations"`
113+
}
114+
115+
// Metric is one entry of a HorizontalPodAutoscaler's spec.metrics.
116+
//
117+
// The API models this as a five-way union — one nested block per source type,
118+
// each with a target of its own. It is flattened here the way an Ingress path
119+
// flattens its backend: Type says which block the manifest wrote, and the
120+
// fields below carry whichever parts of it mean anything. The described object
121+
// on an Object metric is not modelled; such a metric is recorded so a reader
122+
// knows the replica count is driven from somewhere outside the workload, not so
123+
// that it can be resolved.
124+
type Metric struct {
125+
// Type is the metric source: one of the MetricSourceType constants above.
126+
Type value.String `tree:"type"`
127+
128+
// Name is what the metric is called, which is a different thing per Type.
129+
// On Resource and ContainerResource it is the resource name — "cpu" or
130+
// "memory", matching the keys a container's own requests use. On Pods,
131+
// Object and External it is the custom metric's name, which is arbitrary.
132+
Name value.String `tree:"name"`
133+
134+
// ContainerName is the container a ContainerResource metric measures, and
135+
// empty on every other type. This is the container whose request the
136+
// replica count keys off, which on a multi-container pod need not be the
137+
// one a rightsizing recommendation would otherwise pick.
138+
ContainerName value.String `tree:"container_name"`
139+
140+
// TargetType is which kind of target the metric states: one of the
141+
// MetricTargetType constants above.
142+
TargetType value.String `tree:"target_type"`
143+
144+
// TargetUtilization is target.averageUtilization as a percentage, so 50
145+
// means 50%. Set only where TargetType is Utilization, which is the case
146+
// that decides whether observed headroom is waste or the configuration
147+
// working as intended.
148+
//
149+
// Unset serializes as a zero, and zero is not a target anything states, so
150+
// read TargetType rather than testing this for absence.
151+
TargetUtilization value.Int `tree:"target_utilization"`
152+
153+
// TargetValue is target.value or target.averageValue, whichever TargetType
154+
// names, kept as the quantity string the manifest wrote. Custom and
155+
// external metrics carry arbitrary units that nothing here can normalise,
156+
// so this is not converted to a number the way CPU and memory are.
157+
TargetValue value.String `tree:"target_value"`
158+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package autoscaling
2+
3+
import (
4+
"github.com/infracost/go-proto/pkg/tree/kubernetes/meta"
5+
"github.com/infracost/go-proto/pkg/tree/resource"
6+
"github.com/infracost/go-proto/pkg/tree/value"
7+
)
8+
9+
// UpdateMode values for a VerticalPodAutoscaler's spec.updatePolicy.updateMode.
10+
// The distinction that matters is whether the controller writes requests back
11+
// onto pods: only Off leaves the manifest's values in force.
12+
const (
13+
// UpdateModeOff computes recommendations and does nothing with them. The
14+
// manifest's requests are what the pods run with, so editing the manifest
15+
// works normally — a VPA in this mode is an observation, not an obstacle.
16+
UpdateModeOff = "Off"
17+
18+
// UpdateModeInitial applies recommendations at pod creation only. Existing
19+
// pods keep their current requests; the next rollout takes the
20+
// controller's numbers rather than the manifest's.
21+
UpdateModeInitial = "Initial"
22+
23+
// UpdateModeRecreate applies recommendations at pod creation and evicts
24+
// running pods whose requests drift far enough from them.
25+
UpdateModeRecreate = "Recreate"
26+
27+
// UpdateModeAuto is the default when updatePolicy is omitted entirely, and
28+
// currently behaves as Recreate. Note the consequence for parsing: a VPA
29+
// with no updatePolicy block is not "unset" but "Auto", so treating an
30+
// absent mode as harmless gets the common case backwards.
31+
UpdateModeAuto = "Auto"
32+
)
33+
34+
// ContainerPolicyMode values for a container policy's mode field.
35+
const (
36+
// ContainerPolicyModeAuto applies the VPA's recommendations to this
37+
// container. The default when a container policy omits mode.
38+
ContainerPolicyModeAuto = "Auto"
39+
40+
// ContainerPolicyModeOff exempts this container, so its manifest requests
41+
// stand even when the VPA as a whole is in an applying mode. A workload can
42+
// therefore be partly governed: a sidecar left to the VPA and the app
43+
// container opted out, or the reverse.
44+
ContainerPolicyModeOff = "Off"
45+
)
46+
47+
// ControlledValues values for a container policy's controlledValues field — how
48+
// far the controller's writes reach into the container's resources.
49+
const (
50+
// ControlledValuesRequestsAndLimits lets the controller write both requests
51+
// and limits, scaling the limit to preserve the ratio the manifest
52+
// declared. This is the default when the field is omitted, so an absent
53+
// controlledValues means the manifest's limits are overwritten too.
54+
ControlledValuesRequestsAndLimits = "RequestsAndLimits"
55+
56+
// ControlledValuesRequestsOnly leaves limits alone. An edit to a
57+
// container's limits then survives, though an edit to its requests still
58+
// does not.
59+
ControlledValuesRequestsOnly = "RequestsOnly"
60+
)
61+
62+
// VerticalPodAutoscaler is an autoscaling.k8s.io VerticalPodAutoscaler — a CRD
63+
// installed alongside the VPA controller, not a built-in kind.
64+
//
65+
// It provisions nothing and costs nothing. It is in the tree because in every
66+
// mode but Off it sets container requests at admission, which means a
67+
// recommendation that edits the workload's manifest changes a value the cluster
68+
// then overwrites. The pull request merges, the pods keep their old sizes, and
69+
// the finding never resolves. Nothing in the metrics reports that a VPA exists,
70+
// so the manifest is the only place this is visible.
71+
//
72+
// Reading it in the other direction is the useful one: given a workload, is
73+
// there a VPA whose TargetRef names it, and if so does anything exempt the
74+
// container and resource being recommended on — UpdateMode Off for the VPA as a
75+
// whole, or a container policy narrower than that. It decides whether the
76+
// workload is fixable by editing code at all, or whether the recommendation
77+
// belongs in the VPA's own resource policy instead.
78+
//
79+
// The kind, address ([namespace, kind, name]) and source range live on the
80+
// embedded resource.Resource; the VPA's own name and namespace on the embedded
81+
// meta.ObjectMeta; and its Kubernetes labels are stored as the base resource's
82+
// Tags.
83+
type VerticalPodAutoscaler struct {
84+
resource.Resource `tree:"-"`
85+
meta.ObjectMeta `tree:"-"`
86+
87+
// TargetRef is spec.targetRef — the workload this VPA governs. Required by
88+
// the CRD, so an empty value means the manifest is malformed rather than
89+
// that the VPA applies broadly.
90+
TargetRef TargetRef `tree:"target_ref"`
91+
92+
// UpdateMode is spec.updatePolicy.updateMode: one of the UpdateMode
93+
// constants above.
94+
//
95+
// An absent updatePolicy means Auto, not "no mode" — the emptiest possible
96+
// VPA manifest is one of the applying ones. Whether the parser records that
97+
// default explicitly or leaves the value unset for the consumer to
98+
// interpret is the parser's decision; either way an empty value must not be
99+
// read as Off.
100+
UpdateMode value.String `tree:"update_mode"`
101+
102+
// ContainerPolicies is spec.resourcePolicy.containerPolicies, empty when the
103+
// VPA states no per-container policy. Each entry can exempt a container or
104+
// bound what the controller is allowed to set for it.
105+
ContainerPolicies []ContainerPolicy `tree:"container_policies"`
106+
107+
// Annotations are the VPA's Kubernetes annotations, surfaced verbatim.
108+
Annotations []resource.Tag `tree:"annotations"`
109+
}
110+
111+
// ContainerPolicy is one entry of a VerticalPodAutoscaler's
112+
// spec.resourcePolicy.containerPolicies — the per-container overrides on what
113+
// the controller may do.
114+
//
115+
// These matter to a recommendation three times over. Mode decides whether a
116+
// given container is governed at all. ControlledResources and ControlledValues
117+
// narrow that to which of its numbers are governed, so a container can be
118+
// governed for memory and left alone for CPU. And the Min/Max bounds are the
119+
// range the controller will keep it inside — a recommendation outside those
120+
// bounds cannot take effect even where the VPA is applying.
121+
type ContainerPolicy struct {
122+
// ContainerName is the container this policy applies to. The wildcard "*"
123+
// matches every container in the pod, and is how a whole workload is
124+
// exempted with a single entry — so this is not always a real container
125+
// name.
126+
ContainerName value.String `tree:"container_name"`
127+
128+
// Mode is the policy's mode: one of the ContainerPolicyMode constants
129+
// above. Empty means Auto, the CRD's default.
130+
Mode value.String `tree:"mode"`
131+
132+
// ControlledResources is the policy's controlledResources — which of the
133+
// container's resources the controller sets, by resource name ("cpu",
134+
// "memory").
135+
//
136+
// This is Mode narrowed to a single resource: a policy listing only
137+
// "memory" leaves CPU requests alone, so a CPU rightsizing recommendation
138+
// on that container is actionable while a memory one is not. Nil means the
139+
// field was omitted, which defaults to both — so absence is the governing
140+
// case, and must not be read as controlling nothing.
141+
ControlledResources *value.List[string] `tree:"controlled_resources"`
142+
143+
// ControlledValues is the policy's controlledValues: one of the
144+
// ControlledValues constants above. Empty means RequestsAndLimits, the
145+
// CRD's default — so as with UpdateMode an absent value is the wider of the
146+
// two, and reading empty as requests-only gets it backwards.
147+
ControlledValues value.String `tree:"controlled_values"`
148+
149+
// MinAllowed and MaxAllowed bound what the controller may set. Either side
150+
// of either pair may be unset, meaning unbounded in that direction — which
151+
// is why these are not plain numbers; see ResourceAmounts.
152+
MinAllowed ResourceAmounts `tree:"min_allowed"`
153+
MaxAllowed ResourceAmounts `tree:"max_allowed"`
154+
}

0 commit comments

Comments
 (0)