|
| 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 | +} |
0 commit comments