├── .gitignore ├── examples ├── cpuprint │ ├── .gitignore │ ├── deploy.yaml │ ├── README.md │ ├── go.mod │ └── main.go ├── cpureplicaprint │ ├── .gitignore │ ├── deploy.yaml │ ├── go.mod │ ├── README.md │ └── main.go └── cpuandmemoryreplicaprint │ ├── .gitignore │ ├── deploy.yaml │ ├── go.mod │ ├── README.md │ └── main.go ├── staticcheck.conf ├── .github └── workflows │ └── main.yml ├── Makefile ├── internal ├── fake │ ├── doc.go │ ├── resourceclient.go │ ├── replicas.go │ ├── podlist.go │ ├── evaluate.go │ ├── client.go │ └── gather.go ├── pods │ ├── doc.go │ ├── evaluate.go │ ├── evaluate_test.go │ ├── gather.go │ └── gather_test.go ├── object │ ├── doc.go │ ├── evaluate.go │ ├── gather.go │ ├── evaluate_test.go │ └── gather_test.go ├── external │ ├── doc.go │ ├── evaluate.go │ ├── gather.go │ ├── evaluate_test.go │ └── gather_test.go ├── resource │ ├── doc.go │ ├── gather.go │ ├── evaluate.go │ └── evaluate_test.go ├── testutil │ └── testutil.go ├── resourceclient │ ├── resourceclient.go │ └── resourceclient_test.go ├── replicas │ ├── replicas.go │ └── replicas_test.go └── podutil │ └── podutil.go ├── tools.go ├── metrics ├── value │ └── value.go ├── podmetrics │ └── podmetrics.go ├── object │ └── object.go ├── external │ └── external.go ├── metrics.go ├── pods │ └── pods.go └── resource │ └── resource.go ├── doc.go ├── go.mod ├── podsclient └── podsclient.go ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md ├── README.md ├── evaluate.go ├── CONTRIBUTING.md └── metricsclient └── metricsclient.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.out 2 | -------------------------------------------------------------------------------- /examples/cpuprint/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /examples/cpureplicaprint/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /examples/cpuandmemoryreplicaprint/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /staticcheck.conf: -------------------------------------------------------------------------------- 1 | checks = ["all", "-SA1019"] 2 | # We have to disable SA1019 because we need to continue using sets.Strings for backwards compatibility. 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/setup-go@v1 11 | with: 12 | go-version: 1.22 13 | id: go 14 | - name: Check out code into the Go module directory 15 | uses: actions/checkout@v1 16 | - run: go get 17 | - run: make lint 18 | - run: make format && git diff --exit-code 19 | - run: make test 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @echo "=============Running unit tests=============" 3 | go test ./... -cover -coverprofile unit_cover.out 4 | 5 | lint: 6 | @echo "=============Linting=============" 7 | go run honnef.co/go/tools/cmd/staticcheck@v0.4.7 ./... 8 | 9 | format: 10 | @echo "=============Formatting=============" 11 | gofmt -s -w . 12 | go mod tidy 13 | cd examples/cpuandmemoryreplicaprint && go mod tidy && gofmt -s -w . 14 | cd examples/cpureplicaprint && go mod tidy && gofmt -s -w . 15 | cd examples/cpuprint && go mod tidy && gofmt -s -w . 16 | 17 | view_coverage: 18 | @echo "=============Loading coverage HTML=============" 19 | go tool cover -html=unit_cover.out 20 | -------------------------------------------------------------------------------- /internal/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package fake provides stubs for testing. 18 | package fake 19 | -------------------------------------------------------------------------------- /internal/pods/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package pods provides utilities for gathering and evaluating pods metrics 18 | package pods 19 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | /* 4 | Copyright 2023 The K8sHorizMetrics Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | _ "honnef.co/go/tools/cmd/staticcheck" 23 | ) 24 | -------------------------------------------------------------------------------- /internal/object/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package object provides utilities for gathering and evaluating object metrics 18 | package object 19 | -------------------------------------------------------------------------------- /internal/external/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package external provides utilities for gathering and evaluating external metrics 18 | package external 19 | -------------------------------------------------------------------------------- /internal/resource/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package resource provides utilities for gathering and evaluating resource metrics 18 | package resource 19 | -------------------------------------------------------------------------------- /examples/cpuprint/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | run: php-apache 6 | name: php-apache 7 | spec: 8 | replicas: 4 9 | selector: 10 | matchLabels: 11 | run: php-apache 12 | template: 13 | metadata: 14 | labels: 15 | run: php-apache 16 | spec: 17 | containers: 18 | - image: k8s.gcr.io/hpa-example 19 | imagePullPolicy: Always 20 | name: php-apache 21 | ports: 22 | - containerPort: 80 23 | protocol: TCP 24 | resources: 25 | limits: 26 | cpu: 500m 27 | requests: 28 | cpu: 200m 29 | restartPolicy: Always 30 | --- 31 | apiVersion: v1 32 | kind: Service 33 | metadata: 34 | name: php-apache 35 | namespace: default 36 | spec: 37 | ports: 38 | - port: 80 39 | protocol: TCP 40 | targetPort: 80 41 | selector: 42 | run: php-apache 43 | sessionAffinity: None 44 | type: ClusterIP 45 | -------------------------------------------------------------------------------- /examples/cpureplicaprint/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | run: php-apache 6 | name: php-apache 7 | spec: 8 | replicas: 4 9 | selector: 10 | matchLabels: 11 | run: php-apache 12 | template: 13 | metadata: 14 | labels: 15 | run: php-apache 16 | spec: 17 | containers: 18 | - image: k8s.gcr.io/hpa-example 19 | imagePullPolicy: Always 20 | name: php-apache 21 | ports: 22 | - containerPort: 80 23 | protocol: TCP 24 | resources: 25 | limits: 26 | cpu: 500m 27 | requests: 28 | cpu: 200m 29 | restartPolicy: Always 30 | --- 31 | apiVersion: v1 32 | kind: Service 33 | metadata: 34 | name: php-apache 35 | namespace: default 36 | spec: 37 | ports: 38 | - port: 80 39 | protocol: TCP 40 | targetPort: 80 41 | selector: 42 | run: php-apache 43 | sessionAffinity: None 44 | type: ClusterIP 45 | -------------------------------------------------------------------------------- /metrics/value/value.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package value contains models for how K8s metric values are actually defined. 18 | package value 19 | 20 | // MetricValue is a representation of a computed value for a metric, can be either a raw value or an average value 21 | type MetricValue struct { 22 | Value *int64 `json:"value,omitempty"` 23 | AverageValue *int64 `json:"averageValue,omitempty"` 24 | } 25 | -------------------------------------------------------------------------------- /internal/testutil/testutil.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package testutil provides test utilities. 18 | package testutil 19 | 20 | // Int32Ptr returns the provided 32-bit integer as a pointer to a 32-bit integer 21 | func Int32Ptr(i int32) *int32 { 22 | return &i 23 | } 24 | 25 | // Int64Ptr returns the provided 64-bit integer as a pointer to a 64-bit integer 26 | func Int64Ptr(i int64) *int64 { 27 | return &i 28 | } 29 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package k8shorizmetrics provides a simplified interface for gathering metrics and calculating replicas in the same 18 | // way that the Horizontal Pod Autoscaler (HPA) does. 19 | // This is split into two parts, gathering metrics, and evaluating metrics (calculating replicas). 20 | // You can use these parts separately, or together to create a full evaluation process in the same way the HPA does. 21 | package k8shorizmetrics 22 | -------------------------------------------------------------------------------- /examples/cpuandmemoryreplicaprint/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | run: php-apache 6 | name: php-apache 7 | spec: 8 | replicas: 4 9 | selector: 10 | matchLabels: 11 | run: php-apache 12 | template: 13 | metadata: 14 | labels: 15 | run: php-apache 16 | spec: 17 | containers: 18 | - image: k8s.gcr.io/hpa-example 19 | imagePullPolicy: Always 20 | name: php-apache 21 | ports: 22 | - containerPort: 80 23 | protocol: TCP 24 | resources: 25 | limits: 26 | cpu: 500m 27 | memory: 128Mi 28 | requests: 29 | cpu: 200m 30 | memory: 64Mi 31 | restartPolicy: Always 32 | --- 33 | apiVersion: v1 34 | kind: Service 35 | metadata: 36 | name: php-apache 37 | namespace: default 38 | spec: 39 | ports: 40 | - port: 80 41 | protocol: TCP 42 | targetPort: 80 43 | selector: 44 | run: php-apache 45 | sessionAffinity: None 46 | type: ClusterIP 47 | -------------------------------------------------------------------------------- /metrics/podmetrics/podmetrics.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package podmetrics contains models for an individual pod's metrics as returned by the K8s metrics APIs. 18 | package podmetrics 19 | 20 | import "time" 21 | 22 | // Metric contains pod metric value (the metric values are expected to be the metric as a milli-value) 23 | type Metric struct { 24 | Timestamp time.Time `json:"timestamp"` 25 | Window time.Duration `json:"window"` 26 | Value int64 `json:"value"` 27 | } 28 | 29 | // MetricsInfo contains pod metrics as a map from pod names to MetricsInfo 30 | type MetricsInfo map[string]Metric 31 | -------------------------------------------------------------------------------- /metrics/object/object.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package object contains models for object metrics as returned by the K8s metrics APIs. 18 | package object 19 | 20 | import ( 21 | "time" 22 | 23 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/value" 24 | ) 25 | 26 | // Metric (Object) is a metric describing a kubernetes object (for example, hits-per-second on an Ingress object). 27 | type Metric struct { 28 | Current value.MetricValue `json:"current,omitempty"` 29 | ReadyPodCount *int64 `json:"readyPodCount,omitempty"` 30 | Timestamp time.Time `json:"timestamp,omitempty"` 31 | } 32 | -------------------------------------------------------------------------------- /internal/fake/resourceclient.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package fake 18 | 19 | import "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 20 | 21 | // ResourceClient (fake) allows inserting logic into a resource client for testing 22 | type ResourceClient struct { 23 | GetReactor func(apiVersion string, kind string, name string, namespace string) (*unstructured.Unstructured, error) 24 | } 25 | 26 | // Get calls the fake ResourceClient reactor method provided 27 | func (u *ResourceClient) Get(apiVersion string, kind string, name string, namespace string) (*unstructured.Unstructured, error) { 28 | return u.GetReactor(apiVersion, kind, name, namespace) 29 | } 30 | -------------------------------------------------------------------------------- /metrics/external/external.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package external contains models for external metrics as returned by the K8s metrics APIs. 18 | package external 19 | 20 | import ( 21 | "time" 22 | 23 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/value" 24 | ) 25 | 26 | // Metric (Resource) is a global metric that is not associated with any Kubernetes object. It allows autoscaling based 27 | // on information coming from components running outside of cluster (for example length of queue in cloud messaging 28 | // service, or QPS from loadbalancer running outside of cluster). 29 | type Metric struct { 30 | Current value.MetricValue `json:"current,omitempty"` 31 | ReadyPodCount *int64 `json:"readyPodCount,omitempty"` 32 | Timestamp time.Time `json:"timestamp,omitempty"` 33 | } 34 | -------------------------------------------------------------------------------- /metrics/metrics.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package metrics provides models for all of the metrics returned from the K8s APIs grouped into a single model. 18 | package metrics 19 | 20 | import ( 21 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/external" 22 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/object" 23 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/pods" 24 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/resource" 25 | autoscalingv2 "k8s.io/api/autoscaling/v2" 26 | ) 27 | 28 | // Metric is a metric that has been retrieved from the K8s metrics server 29 | type Metric struct { 30 | Spec autoscalingv2.MetricSpec `json:"spec"` 31 | Resource *resource.Metric `json:"resource,omitempty"` 32 | Pods *pods.Metric `json:"pods,omitempty"` 33 | Object *object.Metric `json:"object,omitempty"` 34 | External *external.Metric `json:"external,omitempty"` 35 | } 36 | -------------------------------------------------------------------------------- /metrics/pods/pods.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package pods contains models for metrics relating to a set of pods as returned by the K8s metrics APIs. 18 | package pods 19 | 20 | import ( 21 | "time" 22 | 23 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/podmetrics" 24 | "k8s.io/apimachinery/pkg/util/sets" 25 | ) 26 | 27 | // Metric (Pods) is a metric describing each pod in the current scale target (for example, 28 | // transactions-processed-per-second). The values will be averaged together before being compared to the target value. 29 | type Metric struct { 30 | PodMetricsInfo podmetrics.MetricsInfo `json:"podMetricsInfo"` 31 | ReadyPodCount int64 `json:"readyPodCount"` 32 | IgnoredPods sets.String `json:"ignoredPods"` 33 | MissingPods sets.String `json:"missingPods"` 34 | TotalPods int `json:"totalPods"` 35 | Timestamp time.Time `json:"timestamp,omitempty"` 36 | } 37 | -------------------------------------------------------------------------------- /metrics/resource/resource.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package resource contains models for resource metrics as returned by the K8s metrics APIs. 18 | package resource 19 | 20 | import ( 21 | "time" 22 | 23 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/podmetrics" 24 | "k8s.io/apimachinery/pkg/util/sets" 25 | ) 26 | 27 | // Metric (Resource) is a resource metric known to Kubernetes, as specified in requests and limits, describing each pod 28 | // in the current scale target (e.g. CPU or memory). Such metrics are built in to Kubernetes, and have special scaling 29 | // options on top of those available to normal per-pod metrics (the "pods" source). 30 | type Metric struct { 31 | PodMetricsInfo podmetrics.MetricsInfo `json:"podMetricsInfo"` 32 | Requests map[string]int64 `json:"requests"` 33 | ReadyPodCount int64 `json:"readyPodCount"` 34 | IgnoredPods sets.String `json:"ignoredPods"` 35 | MissingPods sets.String `json:"missingPods"` 36 | TotalPods int `json:"totalPods"` 37 | Timestamp time.Time `json:"timestamp,omitempty"` 38 | } 39 | -------------------------------------------------------------------------------- /internal/fake/replicas.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package fake 18 | 19 | import ( 20 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/podmetrics" 21 | "k8s.io/apimachinery/pkg/util/sets" 22 | ) 23 | 24 | // Calculate (fake) provides a way to insert functionality into a Calculater 25 | type Calculate struct { 26 | GetUsageRatioReplicaCountReactor func(currentReplicas int32, usageRatio float64, readyPodCount int64) int32 27 | GetPlainMetricReplicaCountReactor func(metrics podmetrics.MetricsInfo, 28 | currentReplicas int32, 29 | targetUtilization int64, 30 | readyPodCount int64, 31 | missingPods, 32 | ignoredPods sets.String) int32 33 | } 34 | 35 | // GetUsageRatioReplicaCount calls the fake Calculater function 36 | func (f *Calculate) GetUsageRatioReplicaCount(currentReplicas int32, usageRatio float64, readyPodCount int64) int32 { 37 | return f.GetUsageRatioReplicaCountReactor(currentReplicas, usageRatio, readyPodCount) 38 | } 39 | 40 | // GetPlainMetricReplicaCount calls the fake Calculater function 41 | func (f *Calculate) GetPlainMetricReplicaCount(metrics podmetrics.MetricsInfo, 42 | currentReplicas int32, 43 | targetUtilization int64, 44 | readyPodCount int64, 45 | missingPods, 46 | ignoredPods sets.String) int32 { 47 | return f.GetPlainMetricReplicaCountReactor(metrics, currentReplicas, targetUtilization, readyPodCount, missingPods, ignoredPods) 48 | } 49 | -------------------------------------------------------------------------------- /internal/pods/evaluate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Modifications Copyright 2022 The K8sHorizMetrics Authors. 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | 21 | Modified to split up evaluations and metric gathering to work with the 22 | Custom Pod Autoscaler framework. 23 | Original source: 24 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/horizontal.go 25 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/replica_calculator.go 26 | */ 27 | 28 | package pods 29 | 30 | import ( 31 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/replicas" 32 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics" 33 | ) 34 | 35 | // Evaluate (pods) calculates a replica count evaluation, using the tolerance and calculater provided 36 | type Evaluate struct { 37 | Calculater replicas.Calculator 38 | } 39 | 40 | // Evaluate calculates an evaluation based on the metric provided and the current number of replicas 41 | func (e *Evaluate) Evaluate(currentReplicas int32, gatheredMetric *metrics.Metric) int32 { 42 | return e.Calculater.GetPlainMetricReplicaCount( 43 | gatheredMetric.Pods.PodMetricsInfo, 44 | currentReplicas, 45 | gatheredMetric.Spec.Pods.Target.AverageValue.MilliValue(), 46 | gatheredMetric.Pods.ReadyPodCount, 47 | gatheredMetric.Pods.MissingPods, 48 | gatheredMetric.Pods.IgnoredPods, 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jthomperoo/k8shorizmetrics/v4 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.2 6 | 7 | require ( 8 | github.com/google/go-cmp v0.6.0 9 | honnef.co/go/tools v0.4.7 10 | k8s.io/api v0.30.0 11 | k8s.io/apimachinery v0.30.0 12 | k8s.io/client-go v0.30.0 13 | k8s.io/metrics v0.30.0 14 | ) 15 | 16 | require ( 17 | github.com/BurntSushi/toml v1.3.2 // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/emicklei/go-restful/v3 v3.12.0 // indirect 20 | github.com/evanphx/json-patch v5.9.0+incompatible // indirect 21 | github.com/go-logr/logr v1.4.1 // indirect 22 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 23 | github.com/go-openapi/jsonreference v0.21.0 // indirect 24 | github.com/go-openapi/swag v0.23.0 // indirect 25 | github.com/gogo/protobuf v1.3.2 // indirect 26 | github.com/golang/protobuf v1.5.4 // indirect 27 | github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect 28 | github.com/google/gofuzz v1.2.0 // indirect 29 | github.com/google/uuid v1.6.0 // indirect 30 | github.com/josharian/intern v1.0.0 // indirect 31 | github.com/json-iterator/go v1.1.12 // indirect 32 | github.com/mailru/easyjson v0.7.7 // indirect 33 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 34 | github.com/modern-go/reflect2 v1.0.2 // indirect 35 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 36 | github.com/pkg/errors v0.9.1 // indirect 37 | golang.org/x/exp/typeparams v0.0.0-20240416160154-fe59bbe5cc7f // indirect 38 | golang.org/x/mod v0.17.0 // indirect 39 | golang.org/x/net v0.24.0 // indirect 40 | golang.org/x/oauth2 v0.19.0 // indirect 41 | golang.org/x/sync v0.7.0 // indirect 42 | golang.org/x/sys v0.19.0 // indirect 43 | golang.org/x/term v0.19.0 // indirect 44 | golang.org/x/text v0.14.0 // indirect 45 | golang.org/x/time v0.5.0 // indirect 46 | golang.org/x/tools v0.20.0 // indirect 47 | google.golang.org/protobuf v1.33.0 // indirect 48 | gopkg.in/inf.v0 v0.9.1 // indirect 49 | gopkg.in/yaml.v2 v2.4.0 // indirect 50 | gopkg.in/yaml.v3 v3.0.1 // indirect 51 | k8s.io/klog/v2 v2.120.1 // indirect 52 | k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 // indirect 53 | k8s.io/utils v0.0.0-20240310230437-4693a0247e57 // indirect 54 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 55 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 56 | sigs.k8s.io/yaml v1.4.0 // indirect 57 | ) 58 | -------------------------------------------------------------------------------- /examples/cpuprint/README.md: -------------------------------------------------------------------------------- 1 | # CPU Print 2 | 3 | This example shows how the library can be used to both gather metrics based on metric specs. 4 | 5 | In this example a deployment called `php-apache` is created with 4 replicas that responds to simple HTTP requests 6 | with an `OK!`. The example will query the CPU metrics for the pods in this deployment and print them to stdout. 7 | 8 | > Note this example uses out of cluster configuration of the Kubernetes client, if you want to run this inside the 9 | > cluster you should use in cluster configuration. 10 | 11 | ## Usage 12 | 13 | To follow the steps below and to see this example in action you need the following installed: 14 | 15 | - [Go v1.22+](https://go.dev/doc/install) 16 | - [K3D v5.6+](https://k3d.io/v5.6.0/#installation) 17 | 18 | After you have installed the above you can provision a development Kubernetes cluster by running: 19 | 20 | ```bash 21 | k3d cluster create 22 | ``` 23 | 24 | ### Steps 25 | 26 | Run `go get` to make sure you have all of the dependencies for running the application installed. 27 | 28 | 1. First create the deployment to monitor by applying the deployment YAML: 29 | 30 | ```bash 31 | kubectl apply -f deploy.yaml 32 | ``` 33 | 34 | 2. Run the example using: 35 | 36 | ```bash 37 | go run main.go 38 | ``` 39 | 40 | 3. If you see some errors like this: 41 | 42 | ``` 43 | 2022/05/08 22:26:09 invalid metrics (1 invalid out of 1), first error is: failed to get resource metric: unable to get metrics for resource cpu: no metrics returned from resource metrics API 44 | ``` 45 | 46 | Leave it for a minute or two to let the deployment being targeted (`php-apache`) to generate some CPU metrics with 47 | the metrics server. 48 | 49 | Eventually it should provide output like this: 50 | 51 | ``` 52 | 2022/05/08 22:27:39 CPU statistics: 53 | 2022/05/08 22:27:39 Pod: php-apache-d4cf67d68-s9w2g, CPU usage: 1m (0.50% of requested) 54 | 2022/05/08 22:27:39 Pod: php-apache-d4cf67d68-v9fc2, CPU usage: 1m (0.50% of requested) 55 | 2022/05/08 22:27:39 Pod: php-apache-d4cf67d68-h4z4k, CPU usage: 1m (0.50% of requested) 56 | 2022/05/08 22:27:39 Pod: php-apache-d4cf67d68-jrskj, CPU usage: 1m (0.50% of requested) 57 | 2022/05/08 22:27:39 ---------- 58 | ``` 59 | 60 | 4. Try increasing the CPU load: 61 | 62 | ```bash 63 | kubectl run -it --rm load-generator --image=busybox -- /bin/sh 64 | ``` 65 | 66 | Once it has loaded, run this command to increase CPU load: 67 | 68 | ```bash 69 | while true; do wget -q -O- http://php-apache.default.svc.cluster.local; done 70 | ``` 71 | -------------------------------------------------------------------------------- /internal/fake/podlist.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package fake 18 | 19 | import ( 20 | corev1 "k8s.io/api/core/v1" 21 | "k8s.io/apimachinery/pkg/labels" 22 | corelisters "k8s.io/client-go/listers/core/v1" 23 | ) 24 | 25 | // PodReadyCounter (fake) provides a way to insert functionality into a PodReadyCounter 26 | type PodReadyCounter struct { 27 | GetReadyPodsCountReactor func(namespace string, selector labels.Selector) (int64, error) 28 | } 29 | 30 | // GetReadyPodsCount calls the fake PodReadyCounter function 31 | func (f *PodReadyCounter) GetReadyPodsCount(namespace string, selector labels.Selector) (int64, error) { 32 | return f.GetReadyPodsCountReactor(namespace, selector) 33 | } 34 | 35 | // PodLister (fake) provides a way to insert functionality into a PodLister 36 | type PodLister struct { 37 | ListReactor func(selector labels.Selector) (ret []*corev1.Pod, err error) 38 | PodsReactor func(namespace string) corelisters.PodNamespaceLister 39 | } 40 | 41 | // List calls the fake PodLister function 42 | func (f *PodLister) List(selector labels.Selector) (ret []*corev1.Pod, err error) { 43 | return f.ListReactor(selector) 44 | } 45 | 46 | // Pods calls the fake PodLister function 47 | func (f *PodLister) Pods(namespace string) corelisters.PodNamespaceLister { 48 | return f.PodsReactor(namespace) 49 | } 50 | 51 | // PodNamespaceLister (fake) provides a way to insert functionality into a PodNamespaceLister 52 | type PodNamespaceLister struct { 53 | ListReactor func(selector labels.Selector) (ret []*corev1.Pod, err error) 54 | GetReactor func(name string) (*corev1.Pod, error) 55 | } 56 | 57 | // List calls the fake PodNamespaceLister function 58 | func (f *PodNamespaceLister) List(selector labels.Selector) (ret []*corev1.Pod, err error) { 59 | return f.ListReactor(selector) 60 | } 61 | 62 | // Get calls the fake PodNamespaceLister function 63 | func (f *PodNamespaceLister) Get(name string) (*corev1.Pod, error) { 64 | return f.GetReactor(name) 65 | } 66 | -------------------------------------------------------------------------------- /examples/cpuprint/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jthomperoo/k8shorizmetrics/examples/cpuprint 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.2 6 | 7 | require ( 8 | github.com/jthomperoo/k8shorizmetrics/v4 v4.0.0 9 | k8s.io/api v0.30.0 10 | k8s.io/apimachinery v0.30.0 11 | k8s.io/client-go v0.30.0 12 | ) 13 | 14 | require ( 15 | github.com/BurntSushi/toml v1.3.2 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/emicklei/go-restful/v3 v3.12.0 // indirect 18 | github.com/go-logr/logr v1.4.1 // indirect 19 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 20 | github.com/go-openapi/jsonreference v0.21.0 // indirect 21 | github.com/go-openapi/swag v0.23.0 // indirect 22 | github.com/gogo/protobuf v1.3.2 // indirect 23 | github.com/golang/protobuf v1.5.4 // indirect 24 | github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect 25 | github.com/google/go-cmp v0.6.0 // indirect 26 | github.com/google/gofuzz v1.2.0 // indirect 27 | github.com/google/uuid v1.6.0 // indirect 28 | github.com/imdario/mergo v0.3.6 // indirect 29 | github.com/josharian/intern v1.0.0 // indirect 30 | github.com/json-iterator/go v1.1.12 // indirect 31 | github.com/mailru/easyjson v0.7.7 // indirect 32 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 33 | github.com/modern-go/reflect2 v1.0.2 // indirect 34 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 35 | github.com/spf13/pflag v1.0.5 // indirect 36 | golang.org/x/exp/typeparams v0.0.0-20240416160154-fe59bbe5cc7f // indirect 37 | golang.org/x/mod v0.17.0 // indirect 38 | golang.org/x/net v0.24.0 // indirect 39 | golang.org/x/oauth2 v0.19.0 // indirect 40 | golang.org/x/sync v0.7.0 // indirect 41 | golang.org/x/sys v0.19.0 // indirect 42 | golang.org/x/term v0.19.0 // indirect 43 | golang.org/x/text v0.14.0 // indirect 44 | golang.org/x/time v0.5.0 // indirect 45 | golang.org/x/tools v0.20.0 // indirect 46 | google.golang.org/protobuf v1.33.0 // indirect 47 | gopkg.in/inf.v0 v0.9.1 // indirect 48 | gopkg.in/yaml.v2 v2.4.0 // indirect 49 | gopkg.in/yaml.v3 v3.0.1 // indirect 50 | honnef.co/go/tools v0.4.7 // indirect 51 | k8s.io/klog/v2 v2.120.1 // indirect 52 | k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 // indirect 53 | k8s.io/metrics v0.30.0 // indirect 54 | k8s.io/utils v0.0.0-20240310230437-4693a0247e57 // indirect 55 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 56 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 57 | sigs.k8s.io/yaml v1.4.0 // indirect 58 | ) 59 | 60 | replace github.com/jthomperoo/k8shorizmetrics/v4 => ../../ 61 | -------------------------------------------------------------------------------- /examples/cpureplicaprint/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jthomperoo/k8shorizmetrics/examples/cpureplicaprint 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.2 6 | 7 | require ( 8 | github.com/jthomperoo/k8shorizmetrics/v4 v4.0.0 9 | k8s.io/api v0.30.0 10 | k8s.io/apimachinery v0.30.0 11 | k8s.io/client-go v0.30.0 12 | ) 13 | 14 | require ( 15 | github.com/BurntSushi/toml v1.3.2 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/emicklei/go-restful/v3 v3.12.0 // indirect 18 | github.com/go-logr/logr v1.4.1 // indirect 19 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 20 | github.com/go-openapi/jsonreference v0.21.0 // indirect 21 | github.com/go-openapi/swag v0.23.0 // indirect 22 | github.com/gogo/protobuf v1.3.2 // indirect 23 | github.com/golang/protobuf v1.5.4 // indirect 24 | github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect 25 | github.com/google/go-cmp v0.6.0 // indirect 26 | github.com/google/gofuzz v1.2.0 // indirect 27 | github.com/google/uuid v1.6.0 // indirect 28 | github.com/imdario/mergo v0.3.6 // indirect 29 | github.com/josharian/intern v1.0.0 // indirect 30 | github.com/json-iterator/go v1.1.12 // indirect 31 | github.com/mailru/easyjson v0.7.7 // indirect 32 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 33 | github.com/modern-go/reflect2 v1.0.2 // indirect 34 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 35 | github.com/spf13/pflag v1.0.5 // indirect 36 | golang.org/x/exp/typeparams v0.0.0-20240416160154-fe59bbe5cc7f // indirect 37 | golang.org/x/mod v0.17.0 // indirect 38 | golang.org/x/net v0.24.0 // indirect 39 | golang.org/x/oauth2 v0.19.0 // indirect 40 | golang.org/x/sync v0.7.0 // indirect 41 | golang.org/x/sys v0.19.0 // indirect 42 | golang.org/x/term v0.19.0 // indirect 43 | golang.org/x/text v0.14.0 // indirect 44 | golang.org/x/time v0.5.0 // indirect 45 | golang.org/x/tools v0.20.0 // indirect 46 | google.golang.org/protobuf v1.33.0 // indirect 47 | gopkg.in/inf.v0 v0.9.1 // indirect 48 | gopkg.in/yaml.v2 v2.4.0 // indirect 49 | gopkg.in/yaml.v3 v3.0.1 // indirect 50 | honnef.co/go/tools v0.4.7 // indirect 51 | k8s.io/klog/v2 v2.120.1 // indirect 52 | k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 // indirect 53 | k8s.io/metrics v0.30.0 // indirect 54 | k8s.io/utils v0.0.0-20240310230437-4693a0247e57 // indirect 55 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 56 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 57 | sigs.k8s.io/yaml v1.4.0 // indirect 58 | ) 59 | 60 | replace github.com/jthomperoo/k8shorizmetrics/v4 => ../../ 61 | -------------------------------------------------------------------------------- /examples/cpuandmemoryreplicaprint/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jthomperoo/k8shorizmetrics/examples/cpuandmemoryreplicaprint 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.2 6 | 7 | require ( 8 | github.com/jthomperoo/k8shorizmetrics/v4 v4.0.0 9 | k8s.io/api v0.30.0 10 | k8s.io/apimachinery v0.30.0 11 | k8s.io/client-go v0.30.0 12 | ) 13 | 14 | require ( 15 | github.com/BurntSushi/toml v1.3.2 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/emicklei/go-restful/v3 v3.12.0 // indirect 18 | github.com/go-logr/logr v1.4.1 // indirect 19 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 20 | github.com/go-openapi/jsonreference v0.21.0 // indirect 21 | github.com/go-openapi/swag v0.23.0 // indirect 22 | github.com/gogo/protobuf v1.3.2 // indirect 23 | github.com/golang/protobuf v1.5.4 // indirect 24 | github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect 25 | github.com/google/go-cmp v0.6.0 // indirect 26 | github.com/google/gofuzz v1.2.0 // indirect 27 | github.com/google/uuid v1.6.0 // indirect 28 | github.com/imdario/mergo v0.3.6 // indirect 29 | github.com/josharian/intern v1.0.0 // indirect 30 | github.com/json-iterator/go v1.1.12 // indirect 31 | github.com/mailru/easyjson v0.7.7 // indirect 32 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 33 | github.com/modern-go/reflect2 v1.0.2 // indirect 34 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 35 | github.com/spf13/pflag v1.0.5 // indirect 36 | golang.org/x/exp/typeparams v0.0.0-20240416160154-fe59bbe5cc7f // indirect 37 | golang.org/x/mod v0.17.0 // indirect 38 | golang.org/x/net v0.24.0 // indirect 39 | golang.org/x/oauth2 v0.19.0 // indirect 40 | golang.org/x/sync v0.7.0 // indirect 41 | golang.org/x/sys v0.19.0 // indirect 42 | golang.org/x/term v0.19.0 // indirect 43 | golang.org/x/text v0.14.0 // indirect 44 | golang.org/x/time v0.5.0 // indirect 45 | golang.org/x/tools v0.20.0 // indirect 46 | google.golang.org/protobuf v1.33.0 // indirect 47 | gopkg.in/inf.v0 v0.9.1 // indirect 48 | gopkg.in/yaml.v2 v2.4.0 // indirect 49 | gopkg.in/yaml.v3 v3.0.1 // indirect 50 | honnef.co/go/tools v0.4.7 // indirect 51 | k8s.io/klog/v2 v2.120.1 // indirect 52 | k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 // indirect 53 | k8s.io/metrics v0.30.0 // indirect 54 | k8s.io/utils v0.0.0-20240310230437-4693a0247e57 // indirect 55 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 56 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 57 | sigs.k8s.io/yaml v1.4.0 // indirect 58 | ) 59 | 60 | replace github.com/jthomperoo/k8shorizmetrics/v4 => ../../ 61 | -------------------------------------------------------------------------------- /internal/resourceclient/resourceclient.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package resourceclient provides utilities for retrieving arbitrary K8s resources from the APIs. 18 | package resourceclient 19 | 20 | import ( 21 | "context" 22 | "fmt" 23 | 24 | "strings" 25 | 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 28 | "k8s.io/apimachinery/pkg/runtime/schema" 29 | "k8s.io/client-go/dynamic" 30 | ) 31 | 32 | // Client provides methods for retrieving arbitrary Kubernetes resources, returned as generalised metav1.Object, which 33 | // can be converted to concrete types, and allows for retrieving common and shared data (namespaces, names etc.) 34 | type Client interface { 35 | Get(apiVersion string, kind string, name string, namespace string) (*unstructured.Unstructured, error) 36 | } 37 | 38 | // UnstructuredClient is an implementation of the arbitrary resource client that uses a dynamic Kubernetes interface, 39 | // retrieving unstructured k8s objects and converting them to metav1.Object 40 | type UnstructuredClient struct { 41 | Dynamic dynamic.Interface 42 | } 43 | 44 | // Get takes descriptors of a Kubernetes object (api version, kind, name, namespace) and fetches the matching object, 45 | // returning it as an unstructured Kubernetes resource 46 | func (u *UnstructuredClient) Get(apiVersion string, kind string, name string, namespace string) (*unstructured.Unstructured, error) { 47 | // TODO: update this to be less hacky 48 | // Convert to plural and lowercase 49 | kindPlural := fmt.Sprintf("%ss", strings.ToLower(kind)) 50 | 51 | // Parse group version 52 | resourceGV, err := schema.ParseGroupVersion(apiVersion) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | // Build GVR 58 | resourceGVR := schema.GroupVersionResource{ 59 | Group: resourceGV.Group, 60 | Version: resourceGV.Version, 61 | Resource: kindPlural, 62 | } 63 | 64 | // Get resource 65 | resource, err := u.Dynamic.Resource(resourceGVR).Namespace(namespace).Get(context.Background(), name, metav1.GetOptions{}) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return resource, nil 71 | } 72 | -------------------------------------------------------------------------------- /internal/pods/evaluate_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package pods_test 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/google/go-cmp/cmp" 23 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/fake" 24 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/pods" 25 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/replicas" 26 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics" 27 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/podmetrics" 28 | metricspods "github.com/jthomperoo/k8shorizmetrics/v4/metrics/pods" 29 | v2 "k8s.io/api/autoscaling/v2" 30 | "k8s.io/apimachinery/pkg/api/resource" 31 | "k8s.io/apimachinery/pkg/util/sets" 32 | ) 33 | 34 | func TestEvaluate(t *testing.T) { 35 | var tests = []struct { 36 | description string 37 | expected int32 38 | calculater replicas.Calculator 39 | currentReplicas int32 40 | gatheredMetric *metrics.Metric 41 | }{ 42 | { 43 | "Calculate 5 replicas, 2 ready pods, 1 ignored and 1 missing", 44 | 5, 45 | &fake.Calculate{ 46 | GetPlainMetricReplicaCountReactor: func(metrics podmetrics.MetricsInfo, currentReplicas int32, targetUtilization, readyPodCount int64, missingPods, ignoredPods sets.String) int32 { 47 | return 5 48 | }, 49 | }, 50 | 4, 51 | &metrics.Metric{ 52 | Spec: v2.MetricSpec{ 53 | Pods: &v2.PodsMetricSource{ 54 | Target: v2.MetricTarget{ 55 | AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), 56 | }, 57 | }, 58 | }, 59 | Pods: &metricspods.Metric{ 60 | PodMetricsInfo: podmetrics.MetricsInfo{}, 61 | ReadyPodCount: 2, 62 | IgnoredPods: sets.String{"ignored": {}}, 63 | MissingPods: sets.String{"missing": {}}, 64 | }, 65 | }, 66 | }, 67 | } 68 | for _, test := range tests { 69 | t.Run(test.description, func(t *testing.T) { 70 | eval := pods.Evaluate{ 71 | Calculater: test.calculater, 72 | } 73 | result := eval.Evaluate(test.currentReplicas, test.gatheredMetric) 74 | if !cmp.Equal(test.expected, result) { 75 | t.Errorf("evaluation mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) 76 | } 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /internal/fake/evaluate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package fake 18 | 19 | import ( 20 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics" 21 | ) 22 | 23 | // ExternalEvaluater (fake) provides a way to insert functionality into a ExternalEvaluater 24 | type ExternalEvaluater struct { 25 | EvaluateReactor func(currentReplicas int32, gatheredMetric *metrics.Metric, 26 | tolerance float64) (int32, error) 27 | } 28 | 29 | // Evaluate calls the fake ExternalEvaluater function 30 | func (f *ExternalEvaluater) Evaluate(currentReplicas int32, gatheredMetric *metrics.Metric, 31 | tolerance float64) (int32, error) { 32 | return f.EvaluateReactor(currentReplicas, gatheredMetric, tolerance) 33 | } 34 | 35 | // ObjectEvaluater (fake) provides a way to insert functionality into a ObjectEvaluater 36 | type ObjectEvaluater struct { 37 | EvaluateReactor func(currentReplicas int32, gatheredMetric *metrics.Metric, tolerance float64) (int32, error) 38 | } 39 | 40 | // Evaluate calls the fake ObjectEvaluater function 41 | func (f *ObjectEvaluater) Evaluate(currentReplicas int32, gatheredMetric *metrics.Metric, 42 | tolerance float64) (int32, error) { 43 | return f.EvaluateReactor(currentReplicas, gatheredMetric, tolerance) 44 | } 45 | 46 | // PodsEvaluater (fake) provides a way to insert functionality into a PodsEvaluater 47 | type PodsEvaluater struct { 48 | EvaluateReactor func(currentReplicas int32, gatheredMetric *metrics.Metric) int32 49 | } 50 | 51 | // Evaluate calls the fake PodsEvaluater function 52 | func (f *PodsEvaluater) Evaluate(currentReplicas int32, gatheredMetric *metrics.Metric) int32 { 53 | return f.EvaluateReactor(currentReplicas, gatheredMetric) 54 | } 55 | 56 | // ResourceEvaluater (fake) provides a way to insert functionality into a ResourceEvaluater 57 | type ResourceEvaluater struct { 58 | EvaluateReactor func(currentReplicas int32, gatheredMetric *metrics.Metric, tolerance float64) (int32, error) 59 | } 60 | 61 | // Evaluate calls the fake ResourceEvaluater function 62 | func (f *ResourceEvaluater) Evaluate(currentReplicas int32, gatheredMetric *metrics.Metric, tolerance float64) (int32, error) { 63 | return f.EvaluateReactor(currentReplicas, gatheredMetric, tolerance) 64 | } 65 | -------------------------------------------------------------------------------- /examples/cpureplicaprint/README.md: -------------------------------------------------------------------------------- 1 | # CPU Replica Print 2 | 3 | This example shows how the library can be used to both gather metrics based on metric specs, and then calculate the 4 | replica count that the Horizontal Pod Autoscaler (HPA) would target based on those metrics. 5 | 6 | In this example a deployment called `php-apache` is created with 4 replicas that responds to simple HTTP requests 7 | with an `OK!`. The example will query the CPU metrics for the pods in this deployment, along with the number of 8 | replicas the HPA would target based on those metrics and print them to stdout. 9 | 10 | > Note this example uses out of cluster configuration of the Kubernetes client, if you want to run this inside the 11 | > cluster you should use in cluster configuration. 12 | 13 | ## Usage 14 | 15 | To follow the steps below and to see this example in action you need the following installed: 16 | 17 | - [Go v1.22+](https://go.dev/doc/install) 18 | - [K3D v5.6+](https://k3d.io/v5.6.0/#installation) 19 | 20 | After you have installed the above you can provision a development Kubernetes cluster by running: 21 | 22 | ```bash 23 | k3d cluster create 24 | ``` 25 | 26 | ### Steps 27 | 28 | Run `go get` to make sure you have all of the dependencies for running the application installed. 29 | 30 | 1. First create the deployment to monitor by applying the deployment YAML: 31 | 32 | ```bash 33 | kubectl apply -f deploy.yaml 34 | ``` 35 | 36 | 2. Run the example using: 37 | 38 | ```bash 39 | go run main.go 40 | ``` 41 | 42 | 3. If you see some errors like this: 43 | 44 | ``` 45 | 2022/05/08 22:26:09 invalid metrics (1 invalid out of 1), first error is: failed to get resource metric: unable to get metrics for resource cpu: no metrics returned from resource metrics API 46 | ``` 47 | 48 | Leave it for a minute or two to let the deployment being targeted (`php-apache`) to generate some CPU metrics with 49 | the metrics server. 50 | 51 | Eventually it should provide output like this: 52 | 53 | ``` 54 | 2022/05/08 22:27:39 CPU statistics: 55 | 2022/05/08 22:27:39 Pod: php-apache-d4cf67d68-s9w2g, CPU usage: 1m (0.50% of requested) 56 | 2022/05/08 22:27:39 Pod: php-apache-d4cf67d68-v9fc2, CPU usage: 1m (0.50% of requested) 57 | 2022/05/08 22:27:39 Pod: php-apache-d4cf67d68-h4z4k, CPU usage: 1m (0.50% of requested) 58 | 2022/05/08 22:27:39 Pod: php-apache-d4cf67d68-jrskj, CPU usage: 1m (0.50% of requested) 59 | 2022/05/08 22:27:39 Based on the CPU of the pods the Horizontal Pod Autoscaler would scale from 4 to 0 replicas 60 | 2022/05/08 22:27:39 ---------- 61 | ``` 62 | 63 | 4. Try increasing the CPU load: 64 | 65 | ```bash 66 | kubectl run -it --rm load-generator --image=busybox -- /bin/sh 67 | ``` 68 | 69 | Once it has loaded, run this command to increase CPU load: 70 | 71 | ```bash 72 | while true; do wget -q -O- http://php-apache.default.svc.cluster.local; done 73 | ``` 74 | -------------------------------------------------------------------------------- /internal/fake/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package fake 18 | 19 | import ( 20 | "time" 21 | 22 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/podmetrics" 23 | autoscalingv2 "k8s.io/api/autoscaling/v2" 24 | corev1 "k8s.io/api/core/v1" 25 | "k8s.io/apimachinery/pkg/labels" 26 | ) 27 | 28 | // MetricsClient (fake) provides a way to insert functionality into a metricsclient 29 | type MetricsClient struct { 30 | GetResourceMetricReactor func(resource corev1.ResourceName, namespace string, selector labels.Selector) (podmetrics.MetricsInfo, time.Time, error) 31 | GetRawMetricReactor func(metricName string, namespace string, selector labels.Selector, metricSelector labels.Selector) (podmetrics.MetricsInfo, time.Time, error) 32 | GetObjectMetricReactor func(metricName string, namespace string, objectRef *autoscalingv2.CrossVersionObjectReference, metricSelector labels.Selector) (int64, time.Time, error) 33 | GetExternalMetricReactor func(metricName string, namespace string, selector labels.Selector) ([]int64, time.Time, error) 34 | } 35 | 36 | // GetResourceMetric calls the fake metricsclient function 37 | func (f *MetricsClient) GetResourceMetric(resource corev1.ResourceName, namespace string, selector labels.Selector) (podmetrics.MetricsInfo, time.Time, error) { 38 | return f.GetResourceMetricReactor(resource, namespace, selector) 39 | } 40 | 41 | // GetRawMetric calls the fake metricsclient function 42 | func (f *MetricsClient) GetRawMetric(metricName string, namespace string, selector labels.Selector, metricSelector labels.Selector) (podmetrics.MetricsInfo, time.Time, error) { 43 | return f.GetRawMetricReactor(metricName, namespace, selector, metricSelector) 44 | } 45 | 46 | // GetObjectMetric calls the fake metricsclient function 47 | func (f *MetricsClient) GetObjectMetric(metricName string, namespace string, objectRef *autoscalingv2.CrossVersionObjectReference, metricSelector labels.Selector) (int64, time.Time, error) { 48 | return f.GetObjectMetricReactor(metricName, namespace, objectRef, metricSelector) 49 | } 50 | 51 | // GetExternalMetric calls the fake metricsclient function 52 | func (f *MetricsClient) GetExternalMetric(metricName string, namespace string, selector labels.Selector) ([]int64, time.Time, error) { 53 | return f.GetExternalMetricReactor(metricName, namespace, selector) 54 | } 55 | -------------------------------------------------------------------------------- /internal/object/evaluate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Modifications Copyright 2022 The K8sHorizMetrics Authors. 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | 21 | Modified to split up evaluations and metric gathering to work with the 22 | Custom Pod Autoscaler framework. 23 | Original source: 24 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/horizontal.go 25 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/replica_calculator.go 26 | */ 27 | 28 | package object 29 | 30 | import ( 31 | "fmt" 32 | "math" 33 | 34 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/replicas" 35 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics" 36 | autoscaling "k8s.io/api/autoscaling/v2" 37 | ) 38 | 39 | // Evaluate (object) calculates a replica count evaluation, using the tolerance and calculater provided 40 | type Evaluate struct { 41 | Calculater replicas.Calculator 42 | } 43 | 44 | // Evaluate calculates an evaluation based on the metric provided and the current number of replicas 45 | func (e *Evaluate) Evaluate(currentReplicas int32, gatheredMetric *metrics.Metric, tolerance float64) (int32, error) { 46 | if gatheredMetric.Spec.Object.Target.Type == autoscaling.ValueMetricType { 47 | utilization := float64(*gatheredMetric.Object.Current.Value) 48 | usageRatio := float64(utilization) / float64(gatheredMetric.Spec.Object.Target.Value.MilliValue()) 49 | replicaCount := e.Calculater.GetUsageRatioReplicaCount(currentReplicas, usageRatio, *gatheredMetric.Object.ReadyPodCount) 50 | return replicaCount, nil 51 | } 52 | if gatheredMetric.Spec.Object.Target.Type == autoscaling.AverageValueMetricType { 53 | utilization := float64(*gatheredMetric.Object.Current.AverageValue) 54 | replicaCount := currentReplicas 55 | usageRatio := utilization / (float64(gatheredMetric.Spec.Object.Target.AverageValue.MilliValue()) * float64(replicaCount)) 56 | if math.Abs(1.0-usageRatio) > tolerance { 57 | // update number of replicas if change is large enough 58 | replicaCount = int32(math.Ceil(utilization / float64(gatheredMetric.Spec.Object.Target.AverageValue.MilliValue()))) 59 | } 60 | return replicaCount, nil 61 | } 62 | return 0, fmt.Errorf("invalid object metric source: neither a value target nor an average value target was set") 63 | } 64 | -------------------------------------------------------------------------------- /internal/pods/gather.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Modifications Copyright 2022 The K8sHorizMetrics Authors. 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | 21 | Modified to split up evaluations and metric gathering to work with the 22 | Custom Pod Autoscaler framework. 23 | Original source: 24 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/horizontal.go 25 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/replica_calculator.go 26 | */ 27 | 28 | package pods 29 | 30 | import ( 31 | "fmt" 32 | 33 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/podutil" 34 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/pods" 35 | metricsclient "github.com/jthomperoo/k8shorizmetrics/v4/metricsclient" 36 | corev1 "k8s.io/api/core/v1" 37 | "k8s.io/apimachinery/pkg/labels" 38 | corelisters "k8s.io/client-go/listers/core/v1" 39 | ) 40 | 41 | // Gather (Pods) provides functionality for retrieving metrics for pods metric specs. 42 | type Gather struct { 43 | MetricsClient metricsclient.Client 44 | PodLister corelisters.PodLister 45 | } 46 | 47 | // Gather retrieves a pods metric 48 | func (c *Gather) Gather(metricName string, namespace string, podSelector labels.Selector, metricSelector labels.Selector) (*pods.Metric, error) { 49 | // Get metrics 50 | metrics, timestamp, err := c.MetricsClient.GetRawMetric(metricName, namespace, podSelector, metricSelector) 51 | if err != nil { 52 | return nil, fmt.Errorf("unable to get metric %s: %w", metricName, err) 53 | } 54 | 55 | // Get pods 56 | podList, err := c.PodLister.Pods(namespace).List(podSelector) 57 | if err != nil { 58 | return nil, fmt.Errorf("unable to get pods while calculating replica count: %w", err) 59 | } 60 | 61 | totalPods := len(podList) 62 | if totalPods == 0 { 63 | return &pods.Metric{ 64 | ReadyPodCount: 0, 65 | TotalPods: 0, 66 | Timestamp: timestamp, 67 | }, nil 68 | } 69 | 70 | // Remove missing pod metrics 71 | readyPodCount, _, missingPods := podutil.GroupPods(podList, metrics, corev1.ResourceName(""), 0, 0) 72 | 73 | return &pods.Metric{ 74 | PodMetricsInfo: metrics, 75 | ReadyPodCount: int64(readyPodCount), 76 | IgnoredPods: nil, // Pods metric cannot be CPU based, so Pods cannot be ignored 77 | MissingPods: missingPods, 78 | TotalPods: totalPods, 79 | Timestamp: timestamp, 80 | }, nil 81 | } 82 | -------------------------------------------------------------------------------- /internal/external/evaluate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Modifications Copyright 2022 The K8sHorizMetrics Authors. 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | 21 | Modified to split up evaluations and metric gathering to work with the 22 | Custom Pod Autoscaler framework. 23 | Original source: 24 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/horizontal.go 25 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/replica_calculator.go 26 | */ 27 | 28 | package external 29 | 30 | import ( 31 | "fmt" 32 | "math" 33 | 34 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/replicas" 35 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics" 36 | ) 37 | 38 | // Evaluate (external) calculates a replica count evaluation, using the tolerance and calculater provided 39 | type Evaluate struct { 40 | Calculater replicas.Calculator 41 | } 42 | 43 | // Evaluate calculates an evaluation based on the metric provided and the current number of replicas 44 | func (e *Evaluate) Evaluate(currentReplicas int32, gatheredMetric *metrics.Metric, tolerance float64) (int32, error) { 45 | if gatheredMetric.Spec.External.Target.AverageValue != nil { 46 | utilization := float64(*gatheredMetric.External.Current.AverageValue) 47 | targetUtilizationPerPod := gatheredMetric.Spec.External.Target.AverageValue.MilliValue() 48 | replicaCount := currentReplicas 49 | usageRatio := float64(utilization) / (float64(targetUtilizationPerPod) * float64(replicaCount)) 50 | if math.Abs(1.0-usageRatio) > tolerance { 51 | // update number of replicas if the change is large enough 52 | replicaCount = int32(math.Ceil(float64(utilization) / float64(targetUtilizationPerPod))) 53 | } 54 | return replicaCount, nil 55 | } 56 | 57 | if gatheredMetric.Spec.External.Target.Value != nil { 58 | utilization := float64(*gatheredMetric.External.Current.Value) 59 | replicaCount := currentReplicas 60 | targetUtilization := gatheredMetric.Spec.External.Target.Value.MilliValue() 61 | readyPodCount := gatheredMetric.External.ReadyPodCount 62 | 63 | usageRatio := float64(utilization) / float64(targetUtilization) 64 | replicaCount = e.Calculater.GetUsageRatioReplicaCount(currentReplicas, usageRatio, *readyPodCount) 65 | return replicaCount, nil 66 | } 67 | return 0, fmt.Errorf("invalid external metric source: neither a value target nor an average value target was set") 68 | } 69 | -------------------------------------------------------------------------------- /podsclient/podsclient.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package podsclient provides an on-demand client for retrieving pods, without 18 | // using caching, as the HorizontalPodAutoscaler does. 19 | package podsclient 20 | 21 | import ( 22 | "context" 23 | 24 | corev1 "k8s.io/api/core/v1" 25 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/labels" 27 | "k8s.io/client-go/kubernetes" 28 | corelisters "k8s.io/client-go/listers/core/v1" 29 | ) 30 | 31 | // OnDemandPodNamespaceLister is used to list Pods/get a specific pod in a namespace 32 | type OnDemandPodNamespaceLister struct { 33 | Namespace string 34 | Clientset kubernetes.Interface 35 | } 36 | 37 | // List lists pods that match the selector in the namespace 38 | func (p *OnDemandPodNamespaceLister) List(selector labels.Selector) ([]*corev1.Pod, error) { 39 | pods, err := p.Clientset.CoreV1().Pods(p.Namespace).List(context.Background(), v1.ListOptions{ 40 | LabelSelector: selector.String(), 41 | }) 42 | if err != nil { 43 | return nil, err 44 | } 45 | var podPointers []*corev1.Pod 46 | for i := 0; i < len(pods.Items); i++ { 47 | podPointers = append(podPointers, &pods.Items[i]) 48 | } 49 | return podPointers, nil 50 | } 51 | 52 | // Get gets a single pod with the name provided in the namespace 53 | func (p *OnDemandPodNamespaceLister) Get(name string) (*corev1.Pod, error) { 54 | pod, err := p.Clientset.CoreV1().Pods(p.Namespace).Get(context.Background(), name, v1.GetOptions{}) 55 | if err != nil { 56 | return nil, err 57 | } 58 | return pod, nil 59 | } 60 | 61 | // OnDemandPodLister is used to list Pods across a cluster or retrieve a Namespaced Pod Lister 62 | type OnDemandPodLister struct { 63 | Clientset kubernetes.Interface 64 | } 65 | 66 | // List lists pods that match the selector across the cluster 67 | func (p *OnDemandPodLister) List(selector labels.Selector) ([]*corev1.Pod, error) { 68 | pods, err := p.Clientset.CoreV1().Pods("").List(context.Background(), v1.ListOptions{ 69 | LabelSelector: selector.String(), 70 | }) 71 | if err != nil { 72 | return nil, err 73 | } 74 | var podPointers []*corev1.Pod 75 | for i := 0; i < len(pods.Items); i++ { 76 | podPointers = append(podPointers, &pods.Items[i]) 77 | } 78 | return podPointers, nil 79 | } 80 | 81 | // Pods returns a namespaced pod lister in the namespace provided 82 | func (p *OnDemandPodLister) Pods(namespace string) corelisters.PodNamespaceLister { 83 | return &OnDemandPodNamespaceLister{ 84 | Namespace: namespace, 85 | Clientset: p.Clientset, 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /examples/cpuandmemoryreplicaprint/README.md: -------------------------------------------------------------------------------- 1 | # CPU Replica Print 2 | 3 | This example shows how the library can be used to both gather metrics based on metric specs, and then calculate the 4 | replica count that the Horizontal Pod Autoscaler (HPA) would target based on those metrics. 5 | 6 | This example targets two resource metrics, CPU and memory. 7 | 8 | In this example a deployment called `php-apache` is created with 4 replicas that responds to simple HTTP requests 9 | with an `OK!`. The example will query the CPU and memory metrics for the pods in this deployment, along with the number 10 | of replicas the HPA would target based on those metrics and print them to stdout. 11 | 12 | > Note this example uses out of cluster configuration of the Kubernetes client, if you want to run this inside the 13 | > cluster you should use in cluster configuration. 14 | 15 | ## Usage 16 | 17 | To follow the steps below and to see this example in action you need the following installed: 18 | 19 | - [Go v1.22+](https://go.dev/doc/install) 20 | - [K3D v5.6+](https://k3d.io/v5.6.0/#installation) 21 | 22 | After you have installed the above you can provision a development Kubernetes cluster by running: 23 | 24 | ```bash 25 | k3d cluster create 26 | ``` 27 | 28 | ### Steps 29 | 30 | Run `go get` to make sure you have all of the dependencies for running the application installed. 31 | 32 | 1. First create the deployment to monitor by applying the deployment YAML: 33 | 34 | ```bash 35 | kubectl apply -f deploy.yaml 36 | ``` 37 | 38 | 2. Run the example using: 39 | 40 | ```bash 41 | go run main.go 42 | ``` 43 | 44 | 3. If you see some errors like this: 45 | 46 | ``` 47 | 2022/05/08 22:26:09 invalid metrics (1 invalid out of 1), first error is: failed to get resource metric: unable to get metrics for resource cpu: no metrics returned from resource metrics API 48 | ``` 49 | 50 | Leave it for a minute or two to let the deployment being targeted (`php-apache`) to generate some CPU metrics with 51 | the metrics server. 52 | 53 | Eventually it should provide output like this: 54 | 55 | ``` 56 | 2024/03/17 23:30:03 Pod Metrics: 57 | 2024/03/17 23:30:03 Pod: php-apache-7cb7bd96b4-6rf4f, cpu usage: 1 (0.50% of requested) 58 | 2024/03/17 23:30:03 Pod: php-apache-7cb7bd96b4-7ncxb, cpu usage: 1 (0.50% of requested) 59 | 2024/03/17 23:30:03 Pod: php-apache-7cb7bd96b4-f27b4, cpu usage: 1 (0.50% of requested) 60 | 2024/03/17 23:30:03 Pod: php-apache-7cb7bd96b4-tb8rn, cpu usage: 1 (0.50% of requested) 61 | 2024/03/17 23:30:03 Pod: php-apache-7cb7bd96b4-tb8rn, memory usage: 33853440000 (50.45% of requested) 62 | 2024/03/17 23:30:03 Pod: php-apache-7cb7bd96b4-6rf4f, memory usage: 32382976000 (48.25% of requested) 63 | 2024/03/17 23:30:03 Pod: php-apache-7cb7bd96b4-7ncxb, memory usage: 32739328000 (48.79% of requested) 64 | 2024/03/17 23:30:03 Pod: php-apache-7cb7bd96b4-f27b4, memory usage: 32493568000 (48.42% of requested) 65 | 2024/03/17 23:30:03 The Horizontal Pod Autoscaler would stay at 4 replicas 66 | ``` 67 | 68 | 4. Try increasing the CPU load: 69 | 70 | ```bash 71 | kubectl run -it --rm load-generator --image=busybox -- /bin/sh 72 | ``` 73 | 74 | Once it has loaded, run this command to increase CPU load: 75 | 76 | ```bash 77 | while true; do wget -q -O- http://php-apache.default.svc.cluster.local; done 78 | ``` 79 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct - k8shorizmetrics 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to a positive environment for our 15 | community include: 16 | 17 | * Demonstrating empathy and kindness toward other people 18 | * Being respectful of differing opinions, viewpoints, and experiences 19 | * Giving and gracefully accepting constructive feedback 20 | * Accepting responsibility and apologizing to those affected by our mistakes, 21 | and learning from the experience 22 | * Focusing on what is best not just for us as individuals, but for the 23 | overall community 24 | 25 | Examples of unacceptable behavior include: 26 | 27 | * The use of sexualized language or imagery, and sexual attention or 28 | advances 29 | * Trolling, insulting or derogatory comments, and personal or political attacks 30 | * Public or private harassment 31 | * Publishing others' private information, such as a physical or email 32 | address, without their explicit permission 33 | * Other conduct which could reasonably be considered inappropriate in a 34 | professional setting 35 | 36 | ## Our Responsibilities 37 | 38 | Project maintainers are responsible for clarifying and enforcing our standards of 39 | acceptable behavior and will take appropriate and fair corrective action in 40 | response to any instances of unacceptable behavior. 41 | 42 | Project maintainers have the right and responsibility to remove, edit, or reject 43 | comments, commits, code, wiki edits, issues, and other contributions that are 44 | not aligned to this Code of Conduct, or to ban 45 | temporarily or permanently any contributor for other behaviors that they deem 46 | inappropriate, threatening, offensive, or harmful. 47 | 48 | ## Scope 49 | 50 | This Code of Conduct applies within all community spaces, and also applies when 51 | an individual is officially representing the community in public spaces. 52 | Examples of representing our community include using an official e-mail address, 53 | posting via an official social media account, or acting as an appointed 54 | representative at an online or offline event. 55 | 56 | ## Enforcement 57 | 58 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 59 | reported to the community leaders responsible for enforcement at j.thomperoo@hotmail.com. 60 | All complaints will be reviewed and investigated promptly and fairly. 61 | 62 | All community leaders are obligated to respect the privacy and security of the 63 | reporter of any incident. 64 | 65 | ## Attribution 66 | 67 | This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org/), version 68 | [1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct/code_of_conduct.md) and 69 | [2.0](https://www.contributor-covenant.org/version/2/0/code_of_conduct/code_of_conduct.md), 70 | and was generated by [contributing-gen](https://github.com/bttger/contributing-gen). 71 | -------------------------------------------------------------------------------- /internal/object/gather.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Modifications Copyright 2022 The K8sHorizMetrics Authors. 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | 21 | Modified to split up evaluations and metric gathering to work with the 22 | Custom Pod Autoscaler framework. 23 | Original source: 24 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/horizontal.go 25 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/replica_calculator.go 26 | */ 27 | 28 | package object 29 | 30 | import ( 31 | "fmt" 32 | 33 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/podutil" 34 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/object" 35 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/value" 36 | metricsclient "github.com/jthomperoo/k8shorizmetrics/v4/metricsclient" 37 | autoscaling "k8s.io/api/autoscaling/v2" 38 | "k8s.io/apimachinery/pkg/labels" 39 | ) 40 | 41 | // Gather (Object) provides functionality for retrieving metrics for object metric specs. 42 | type Gather struct { 43 | MetricsClient metricsclient.Client 44 | PodReadyCounter podutil.PodReadyCounter 45 | } 46 | 47 | // Gather retrieves an object metric 48 | func (c *Gather) Gather(metricName string, namespace string, objectRef *autoscaling.CrossVersionObjectReference, podSelector labels.Selector, metricSelector labels.Selector) (*object.Metric, error) { 49 | // Get metrics 50 | utilization, timestamp, err := c.MetricsClient.GetObjectMetric(metricName, namespace, objectRef, metricSelector) 51 | if err != nil { 52 | return nil, fmt.Errorf("unable to get metric %s: %s on %s %s: %w", metricName, objectRef.Kind, namespace, objectRef.Name, err) 53 | } 54 | 55 | // Calculate number of ready pods 56 | readyPodCount, err := c.PodReadyCounter.GetReadyPodsCount(namespace, podSelector) 57 | if err != nil { 58 | return nil, fmt.Errorf("unable to calculate ready pods: %w", err) 59 | } 60 | 61 | return &object.Metric{ 62 | Current: value.MetricValue{ 63 | Value: &utilization, 64 | }, 65 | ReadyPodCount: &readyPodCount, 66 | Timestamp: timestamp, 67 | }, nil 68 | } 69 | 70 | // GatherPerPod retrieves an object per pod metric 71 | func (c *Gather) GatherPerPod(metricName string, namespace string, objectRef *autoscaling.CrossVersionObjectReference, metricSelector labels.Selector) (*object.Metric, error) { 72 | // Get metrics 73 | utilization, timestamp, err := c.MetricsClient.GetObjectMetric(metricName, namespace, objectRef, metricSelector) 74 | if err != nil { 75 | return nil, fmt.Errorf("unable to get metric %s: %s on %s %s/%w", metricName, objectRef.Kind, namespace, objectRef.Name, err) 76 | } 77 | 78 | return &object.Metric{ 79 | Current: value.MetricValue{ 80 | AverageValue: &utilization, 81 | }, 82 | Timestamp: timestamp, 83 | }, nil 84 | } 85 | -------------------------------------------------------------------------------- /examples/cpuprint/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "log" 21 | "path/filepath" 22 | "time" 23 | 24 | "github.com/jthomperoo/k8shorizmetrics/v4" 25 | "github.com/jthomperoo/k8shorizmetrics/v4/metricsclient" 26 | "github.com/jthomperoo/k8shorizmetrics/v4/podsclient" 27 | v2 "k8s.io/api/autoscaling/v2" 28 | corev1 "k8s.io/api/core/v1" 29 | "k8s.io/apimachinery/pkg/labels" 30 | "k8s.io/client-go/kubernetes" 31 | "k8s.io/client-go/tools/clientcmd" 32 | "k8s.io/client-go/util/homedir" 33 | ) 34 | 35 | const ( 36 | cpuInitializationPeriodSeconds = 300 37 | initialReadinessDelaySeconds = 30 38 | namespace = "default" 39 | ) 40 | 41 | var podMatchSelector = labels.SelectorFromSet(labels.Set{ 42 | "run": "php-apache", 43 | }) 44 | 45 | func main() { 46 | clusterConfig, err := clientcmd.BuildConfigFromFlags("", filepath.Join(homedir.HomeDir(), ".kube", "config")) 47 | if err != nil { 48 | log.Fatalf("Fail to create out-of-cluster Kubernetes config: %s", err) 49 | } 50 | 51 | clientset, err := kubernetes.NewForConfig(clusterConfig) 52 | if err != nil { 53 | log.Fatalf("Fail to set up Kubernetes clientset: %s", err) 54 | } 55 | 56 | metricsclient := metricsclient.NewClient(clusterConfig, clientset.Discovery()) 57 | podsclient := &podsclient.OnDemandPodLister{ 58 | Clientset: clientset, 59 | } 60 | cpuInitializationPeriod := time.Duration(cpuInitializationPeriodSeconds) * time.Second 61 | initialReadinessDelay := time.Duration(initialReadinessDelaySeconds) * time.Second 62 | 63 | // Set up the metric gatherer, needs to be able to query metrics and pods with the clients provided, along with 64 | // config options 65 | gather := k8shorizmetrics.NewGatherer(metricsclient, podsclient, cpuInitializationPeriod, initialReadinessDelay) 66 | 67 | // This is the metric spec, this targets the CPU resource metric, gathering utilization values 68 | // Equivalent to the following YAML: 69 | // metrics: 70 | // - type: Resource 71 | // resource: 72 | // name: cpu 73 | // target: 74 | // type: Utilization 75 | spec := v2.MetricSpec{ 76 | Type: v2.ResourceMetricSourceType, 77 | Resource: &v2.ResourceMetricSource{ 78 | Name: corev1.ResourceCPU, 79 | Target: v2.MetricTarget{ 80 | Type: v2.UtilizationMetricType, 81 | }, 82 | }, 83 | } 84 | 85 | // Loop infinitely, wait 5 seconds between each loop 86 | for { 87 | time.Sleep(5 * time.Second) 88 | 89 | // Gather the metrics using the specs, targeting the namespace and pod selector defined above 90 | metric, err := gather.GatherSingleMetric(spec, namespace, podMatchSelector) 91 | if err != nil { 92 | log.Println(err) 93 | continue 94 | } 95 | 96 | log.Println("CPU metrics:") 97 | 98 | for pod, podmetric := range metric.Resource.PodMetricsInfo { 99 | actualCPU := podmetric.Value 100 | requestedCPU := metric.Resource.Requests[pod] 101 | log.Printf("Pod: %s, CPU usage: %dm (%0.2f%% of requested)\n", pod, actualCPU, float64(actualCPU)/float64(requestedCPU)*100.0) 102 | } 103 | 104 | log.Println("----------") 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /internal/external/gather.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Modifications Copyright 2022 The K8sHorizMetrics Authors. 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | 21 | Modified to split up evaluations and metric gathering to work with the 22 | Custom Pod Autoscaler framework. 23 | Original source: 24 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/horizontal.go 25 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/replica_calculator.go 26 | */ 27 | 28 | package external 29 | 30 | import ( 31 | "fmt" 32 | 33 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/podutil" 34 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/external" 35 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/value" 36 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 37 | 38 | metricsclient "github.com/jthomperoo/k8shorizmetrics/v4/metricsclient" 39 | "k8s.io/apimachinery/pkg/labels" 40 | ) 41 | 42 | // Gather (External) provides functionality for retrieving metrics for external metric specs. 43 | type Gather struct { 44 | MetricsClient metricsclient.Client 45 | PodReadyCounter podutil.PodReadyCounter 46 | } 47 | 48 | // Gather retrieves an external metric 49 | func (c *Gather) Gather(metricName, namespace string, metricSelector *metav1.LabelSelector, podSelector labels.Selector) (*external.Metric, error) { 50 | // Convert selector to expected type 51 | metricLabelSelector, err := metav1.LabelSelectorAsSelector(metricSelector) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | // Get metrics 57 | gathered, timestamp, err := c.MetricsClient.GetExternalMetric(metricName, namespace, metricLabelSelector) 58 | if err != nil { 59 | return nil, fmt.Errorf("unable to get external metric %s/%s/%+v: %w", namespace, metricName, metricSelector, err) 60 | } 61 | utilization := int64(0) 62 | for _, val := range gathered { 63 | utilization = utilization + val 64 | } 65 | 66 | // Calculate number of ready pods 67 | readyPodCount, err := c.PodReadyCounter.GetReadyPodsCount(namespace, podSelector) 68 | if err != nil { 69 | return nil, fmt.Errorf("unable to calculate ready pods: %w", err) 70 | } 71 | 72 | return &external.Metric{ 73 | Current: value.MetricValue{ 74 | Value: &utilization, 75 | }, 76 | ReadyPodCount: &readyPodCount, 77 | Timestamp: timestamp, 78 | }, nil 79 | } 80 | 81 | // GatherPerPod retrieves an external per pod metric 82 | func (c *Gather) GatherPerPod(metricName, namespace string, metricSelector *metav1.LabelSelector) (*external.Metric, error) { 83 | // Convert selector to expected type 84 | metricLabelSelector, err := metav1.LabelSelectorAsSelector(metricSelector) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | // Get metrics 90 | gathered, timestamp, err := c.MetricsClient.GetExternalMetric(metricName, namespace, metricLabelSelector) 91 | if err != nil { 92 | return nil, fmt.Errorf("unable to get external metric %s/%s/%+v: %w", namespace, metricName, metricSelector, err) 93 | } 94 | 95 | // Calculate utilization total for pods 96 | utilization := int64(0) 97 | for _, val := range gathered { 98 | utilization = utilization + val 99 | } 100 | 101 | return &external.Metric{ 102 | Current: value.MetricValue{ 103 | AverageValue: &utilization, 104 | }, 105 | Timestamp: timestamp, 106 | }, nil 107 | } 108 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [v4.0.0] - 2024-04-21 10 | ### Changed 11 | - **BREAKING CHANGE** Changed module path from `github.com/jthomperoo/k8shorizmetrics/v3` to 12 | `github.com/jthomperoo/k8shorizmetrics/v4`. 13 | - **BREAKING CHANGE** Types now use JSON tags which match Kubernetes convention, with naming using camel case rather 14 | than snake case. For example the Resource Metric field `PodMetricsInfo` is now serialised as `podMetricsInfo` rather 15 | than `pod_metrics_info`. 16 | - Updated minimum Go version to `v1.22`. 17 | 18 | ## [v3.0.0] - 2024-03-21 19 | ### Changed 20 | - **BREAKING CHANGE** Changed module path from `github.com/jthomperoo/k8shorizmetrics/v2` to 21 | `github.com/jthomperoo/k8shorizmetrics/v3`. 22 | - **BREAKING CHANGE** Gather now returns the `GathererMultiMetricError` error type if any of the metrics fail to 23 | gather. This error is returned for partial errors, meaning some metrics gathered successfully and others did not. 24 | If this partial error occurs the `GathererMultiMetricError` error will have the `Partial` property set to `true`. This 25 | can be checked for using `errors.As`. 26 | - **BREAKING CHANGE** Evaluate now returns the `EvaluatorMultiMetricError` error type if any of the metrics fail to 27 | evaluate. This error is returned for partial errors, meaning some metrics evaluted successfully and others did not. 28 | If this partial error occurs the `EvaluatorMultiMetricError` error will have the `Partial` property set to `true`. This 29 | can be checked for using `errors.As`. 30 | 31 | ## [v2.0.2] - 2023-12-23 32 | ### Changed 33 | - Upgraded to Go `v1.21`. 34 | - Upgraded package dependencies. 35 | 36 | ## [v2.0.1] - 2023-03-07 37 | ### Changed 38 | - Upgraded to Go `v1.20`. 39 | - Upgraded package dependencies. 40 | 41 | ## [v2.0.0] - 2022-12-02 42 | ### Changed 43 | - **BREAKING CHANGE** Upgraded from Kubernetes `autoscaling/v2beta2` to the graduated `autoscaling/v2`. This drops 44 | support for Kubernetes versions `v1.22` and below. 45 | - **BREAKING CHANGE** Changed module path from `github.com/jthomperoo/k8shorizmetrics` to 46 | `github.com/jthomperoo/k8shorizmetrics/v2`. 47 | - Upgraded to Kubernetes client libaries `v0.25.4` to support Kubernetes `v1.23+`. 48 | - Upgraded to Go `v1.19`. 49 | 50 | 51 | ## [v1.1.0] - 2022-12-02 52 | ### Added 53 | - New `GatherWithOptions` and `GatherSingleMetricWithOptions` methods which allow you to provide the CPU initialization 54 | and delay of initial readiness status at call time as parameters. 55 | - New `EvaluateWithOptions` and `EvaluateSingleMetricWithOptions` methods which allow you to provide the tolerance at 56 | call time as a parameter. 57 | - `CPUInitializationPeriod` and `DelayOfInitialReadinessStatus` now exposed as member variables of the `Gatherer`. 58 | - `Tolerance` now exposed as a member variable of the `Evaluator`. 59 | 60 | ## [v1.0.0] - 2022-05-14 61 | ### Added 62 | - Simple API, based directly on the code from the HPA, but detangled for ease of use. 63 | - Dependent only on versioned and public Kubernetes Golang modules, allows easy install without replace directives. 64 | - Splits the HPA into two parts, metric gathering and evaluation, only use what you need. 65 | - Allows insights into how the HPA makes decisions. 66 | - Supports scaling to and from 0. 67 | 68 | [Unreleased]: https://github.com/jthomperoo/k8shorizmetrics/compare/v4.0.0...HEAD 69 | [v4.0.0]: https://github.com/jthomperoo/k8shorizmetrics/compare/v3.0.0...v4.0.0 70 | [v3.0.0]: https://github.com/jthomperoo/k8shorizmetrics/compare/v2.0.2...v3.0.0 71 | [v2.0.2]: https://github.com/jthomperoo/k8shorizmetrics/compare/v2.0.1...v2.0.2 72 | [v2.0.1]: https://github.com/jthomperoo/k8shorizmetrics/compare/v2.0.0...v2.0.1 73 | [v2.0.0]: https://github.com/jthomperoo/k8shorizmetrics/compare/v1.1.0...v2.0.0 74 | [v1.1.0]: https://github.com/jthomperoo/k8shorizmetrics/compare/v1.0.0...v1.1.0 75 | [v1.0.0]: https://github.com/jthomperoo/k8shorizmetrics/releases/tag/v1.0.0 76 | -------------------------------------------------------------------------------- /internal/external/evaluate_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package external_test 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | 23 | "github.com/google/go-cmp/cmp" 24 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/external" 25 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/fake" 26 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/replicas" 27 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/testutil" 28 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics" 29 | externalmetrics "github.com/jthomperoo/k8shorizmetrics/v4/metrics/external" 30 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/value" 31 | v2 "k8s.io/api/autoscaling/v2" 32 | "k8s.io/apimachinery/pkg/api/resource" 33 | ) 34 | 35 | func TestEvaluate(t *testing.T) { 36 | equateErrorMessage := cmp.Comparer(func(x, y error) bool { 37 | if x == nil || y == nil { 38 | return x == nil && y == nil 39 | } 40 | return x.Error() == y.Error() 41 | }) 42 | 43 | var tests = []struct { 44 | description string 45 | expected int32 46 | expectedErr error 47 | calculater replicas.Calculator 48 | tolerance float64 49 | currentReplicas int32 50 | gatheredMetric *metrics.Metric 51 | }{ 52 | { 53 | "Invalid metric source", 54 | 0, 55 | errors.New("invalid external metric source: neither a value target nor an average value target was set"), 56 | nil, 57 | 0, 58 | 3, 59 | &metrics.Metric{ 60 | Spec: v2.MetricSpec{ 61 | External: &v2.ExternalMetricSource{}, 62 | }, 63 | }, 64 | }, 65 | { 66 | "Success, average value, beyond tolerance", 67 | 10, 68 | nil, 69 | nil, 70 | 0, 71 | 5, 72 | &metrics.Metric{ 73 | Spec: v2.MetricSpec{ 74 | External: &v2.ExternalMetricSource{ 75 | Target: v2.MetricTarget{ 76 | AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), 77 | }, 78 | }, 79 | }, 80 | External: &externalmetrics.Metric{ 81 | Current: value.MetricValue{ 82 | AverageValue: testutil.Int64Ptr(500), 83 | }, 84 | }, 85 | }, 86 | }, 87 | { 88 | "Success, average value, within tolerance", 89 | 5, 90 | nil, 91 | nil, 92 | 0, 93 | 5, 94 | &metrics.Metric{ 95 | Spec: v2.MetricSpec{ 96 | External: &v2.ExternalMetricSource{ 97 | Target: v2.MetricTarget{ 98 | AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), 99 | }, 100 | }, 101 | }, 102 | External: &externalmetrics.Metric{ 103 | Current: value.MetricValue{ 104 | AverageValue: testutil.Int64Ptr(250), 105 | }, 106 | }, 107 | }, 108 | }, 109 | { 110 | "Success, value", 111 | 3, 112 | nil, 113 | &fake.Calculate{ 114 | GetUsageRatioReplicaCountReactor: func(currentReplicas int32, usageRatio float64, readyPodCount int64) int32 { 115 | return 3 116 | }, 117 | }, 118 | 0, 119 | 5, 120 | &metrics.Metric{ 121 | Spec: v2.MetricSpec{ 122 | External: &v2.ExternalMetricSource{ 123 | Target: v2.MetricTarget{ 124 | Value: resource.NewMilliQuantity(50, resource.DecimalSI), 125 | }, 126 | }, 127 | }, 128 | External: &externalmetrics.Metric{ 129 | ReadyPodCount: testutil.Int64Ptr(2), 130 | Current: value.MetricValue{ 131 | Value: testutil.Int64Ptr(250), 132 | }, 133 | }, 134 | }, 135 | }, 136 | } 137 | for _, test := range tests { 138 | t.Run(test.description, func(t *testing.T) { 139 | evaluater := external.Evaluate{ 140 | Calculater: test.calculater, 141 | } 142 | evaluation, err := evaluater.Evaluate(test.currentReplicas, test.gatheredMetric, test.tolerance) 143 | if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { 144 | t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) 145 | return 146 | } 147 | if !cmp.Equal(test.expected, evaluation) { 148 | t.Errorf("evaluation mismatch (-want +got):\n%s", cmp.Diff(test.expected, evaluation)) 149 | } 150 | }) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /internal/object/evaluate_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package object_test 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | 23 | "github.com/google/go-cmp/cmp" 24 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/fake" 25 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/object" 26 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/replicas" 27 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/testutil" 28 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics" 29 | objectmetrics "github.com/jthomperoo/k8shorizmetrics/v4/metrics/object" 30 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/value" 31 | v2 "k8s.io/api/autoscaling/v2" 32 | "k8s.io/apimachinery/pkg/api/resource" 33 | ) 34 | 35 | func TestEvaluate(t *testing.T) { 36 | equateErrorMessage := cmp.Comparer(func(x, y error) bool { 37 | if x == nil || y == nil { 38 | return x == nil && y == nil 39 | } 40 | return x.Error() == y.Error() 41 | }) 42 | 43 | var tests = []struct { 44 | description string 45 | expected int32 46 | expectedErr error 47 | calculater replicas.Calculator 48 | tolerance float64 49 | currentReplicas int32 50 | gatheredMetric *metrics.Metric 51 | }{ 52 | { 53 | "Invalid metric source", 54 | 0, 55 | errors.New("invalid object metric source: neither a value target nor an average value target was set"), 56 | nil, 57 | 0, 58 | 3, 59 | &metrics.Metric{ 60 | Spec: v2.MetricSpec{ 61 | Object: &v2.ObjectMetricSource{}, 62 | }, 63 | }, 64 | }, 65 | { 66 | "Success, average value, beyond tolerance", 67 | 10, 68 | nil, 69 | nil, 70 | 0, 71 | 5, 72 | &metrics.Metric{ 73 | Spec: v2.MetricSpec{ 74 | Object: &v2.ObjectMetricSource{ 75 | Target: v2.MetricTarget{ 76 | Type: v2.AverageValueMetricType, 77 | AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), 78 | }, 79 | }, 80 | }, 81 | Object: &objectmetrics.Metric{ 82 | Current: value.MetricValue{ 83 | AverageValue: testutil.Int64Ptr(500), 84 | }, 85 | }, 86 | }, 87 | }, 88 | { 89 | "Success, average value, within tolerance", 90 | 5, 91 | nil, 92 | nil, 93 | 0, 94 | 5, 95 | &metrics.Metric{ 96 | Spec: v2.MetricSpec{ 97 | Object: &v2.ObjectMetricSource{ 98 | Target: v2.MetricTarget{ 99 | Type: v2.AverageValueMetricType, 100 | AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), 101 | }, 102 | }, 103 | }, 104 | Object: &objectmetrics.Metric{ 105 | Current: value.MetricValue{ 106 | AverageValue: testutil.Int64Ptr(250), 107 | }, 108 | }, 109 | }, 110 | }, 111 | { 112 | "Success, value", 113 | 3, 114 | nil, 115 | &fake.Calculate{ 116 | GetUsageRatioReplicaCountReactor: func(currentReplicas int32, usageRatio float64, readyPodCount int64) int32 { 117 | return 3 118 | }, 119 | }, 120 | 0, 121 | 5, 122 | &metrics.Metric{ 123 | Spec: v2.MetricSpec{ 124 | Object: &v2.ObjectMetricSource{ 125 | Target: v2.MetricTarget{ 126 | Type: v2.ValueMetricType, 127 | Value: resource.NewMilliQuantity(50, resource.DecimalSI), 128 | }, 129 | }, 130 | }, 131 | Object: &objectmetrics.Metric{ 132 | ReadyPodCount: testutil.Int64Ptr(2), 133 | Current: value.MetricValue{ 134 | Value: testutil.Int64Ptr(250), 135 | }, 136 | }, 137 | }, 138 | }, 139 | } 140 | for _, test := range tests { 141 | t.Run(test.description, func(t *testing.T) { 142 | evaluater := object.Evaluate{ 143 | Calculater: test.calculater, 144 | } 145 | evaluation, err := evaluater.Evaluate(test.currentReplicas, test.gatheredMetric, test.tolerance) 146 | if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { 147 | t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) 148 | return 149 | } 150 | if !cmp.Equal(test.expected, evaluation) { 151 | t.Errorf("evaluation mismatch (-want +got):\n%s", cmp.Diff(test.expected, evaluation)) 152 | } 153 | }) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /internal/resource/gather.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Modifications Copyright 2022 The K8sHorizMetrics Authors. 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | 21 | Modified to split up evaluations and metric gathering to work with the 22 | Custom Pod Autoscaler framework. 23 | Original source: 24 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/horizontal.go 25 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/replica_calculator.go 26 | */ 27 | 28 | package resource 29 | 30 | import ( 31 | "fmt" 32 | "time" 33 | 34 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/podutil" 35 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/resource" 36 | metricsclient "github.com/jthomperoo/k8shorizmetrics/v4/metricsclient" 37 | corev1 "k8s.io/api/core/v1" 38 | "k8s.io/apimachinery/pkg/labels" 39 | corelisters "k8s.io/client-go/listers/core/v1" 40 | ) 41 | 42 | // Gather (Resource) provides functionality for retrieving metrics for resource metric specs. 43 | type Gather struct { 44 | MetricsClient metricsclient.Client 45 | PodLister corelisters.PodLister 46 | } 47 | 48 | // Gather retrieves a resource metric 49 | func (c *Gather) Gather(resourceName corev1.ResourceName, namespace string, podSelector labels.Selector, 50 | cpuInitializationPeriod time.Duration, delayOfInitialReadinessStatus time.Duration) (*resource.Metric, error) { 51 | // Get metrics 52 | metrics, timestamp, err := c.MetricsClient.GetResourceMetric(resourceName, namespace, podSelector) 53 | if err != nil { 54 | return nil, fmt.Errorf("unable to get metrics for resource %s: %w", resourceName, err) 55 | } 56 | 57 | // Get pods 58 | podList, err := c.PodLister.Pods(namespace).List(podSelector) 59 | if err != nil { 60 | return nil, fmt.Errorf("unable to get pods while calculating replica count: %w", err) 61 | } 62 | 63 | totalPods := len(podList) 64 | if totalPods == 0 { 65 | return nil, fmt.Errorf("no pods returned by selector while calculating replica count") 66 | } 67 | 68 | // Remove missing pod metrics 69 | readyPodCount, ignoredPods, missingPods := podutil.GroupPods(podList, metrics, resourceName, cpuInitializationPeriod, delayOfInitialReadinessStatus) 70 | podutil.RemoveMetricsForPods(metrics, ignoredPods) 71 | 72 | // Calculate requests - limits for pod resources 73 | requests, err := podutil.CalculatePodRequests(podList, resourceName) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | return &resource.Metric{ 79 | PodMetricsInfo: metrics, 80 | Requests: requests, 81 | ReadyPodCount: int64(readyPodCount), 82 | IgnoredPods: ignoredPods, 83 | MissingPods: missingPods, 84 | TotalPods: totalPods, 85 | Timestamp: timestamp, 86 | }, nil 87 | } 88 | 89 | // GatherRaw retrieves a a raw resource metric 90 | func (c *Gather) GatherRaw(resourceName corev1.ResourceName, namespace string, podSelector labels.Selector, 91 | cpuInitializationPeriod time.Duration, delayOfInitialReadinessStatus time.Duration) (*resource.Metric, error) { 92 | // Get metrics 93 | metrics, timestamp, err := c.MetricsClient.GetResourceMetric(resourceName, namespace, podSelector) 94 | if err != nil { 95 | return nil, fmt.Errorf("unable to get metrics for resource %s: %w", resourceName, err) 96 | } 97 | 98 | // Get pods 99 | podList, err := c.PodLister.Pods(namespace).List(podSelector) 100 | if err != nil { 101 | return nil, fmt.Errorf("unable to get pods while calculating replica count: %w", err) 102 | } 103 | 104 | totalPods := len(podList) 105 | if totalPods == 0 { 106 | return nil, fmt.Errorf("no pods returned by selector while calculating replica count") 107 | } 108 | 109 | // Remove missing pod metrics 110 | readyPodCount, ignoredPods, missingPods := podutil.GroupPods(podList, metrics, resourceName, cpuInitializationPeriod, delayOfInitialReadinessStatus) 111 | podutil.RemoveMetricsForPods(metrics, ignoredPods) 112 | 113 | return &resource.Metric{ 114 | PodMetricsInfo: metrics, 115 | ReadyPodCount: int64(readyPodCount), 116 | IgnoredPods: ignoredPods, 117 | MissingPods: missingPods, 118 | TotalPods: totalPods, 119 | Timestamp: timestamp, 120 | }, nil 121 | } 122 | -------------------------------------------------------------------------------- /internal/resourceclient/resourceclient_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package resourceclient_test 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | 23 | "github.com/google/go-cmp/cmp" 24 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/resourceclient" 25 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 26 | k8sruntime "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/client-go/dynamic" 28 | "k8s.io/client-go/dynamic/fake" 29 | k8stesting "k8s.io/client-go/testing" 30 | ) 31 | 32 | func TestClient_Get(t *testing.T) { 33 | equateErrorMessage := cmp.Comparer(func(x, y error) bool { 34 | if x == nil || y == nil { 35 | return x == nil && y == nil 36 | } 37 | return x.Error() == y.Error() 38 | }) 39 | 40 | var tests = []struct { 41 | description string 42 | expected *unstructured.Unstructured 43 | expectedErr error 44 | dynamic dynamic.Interface 45 | unstructuredConverter k8sruntime.UnstructuredConverter 46 | apiVersion string 47 | kind string 48 | name string 49 | namespace string 50 | }{ 51 | { 52 | "Invalid group version, fail to parse", 53 | nil, 54 | errors.New(`unexpected GroupVersion string: /invalid/`), 55 | nil, 56 | nil, 57 | "/invalid/", 58 | "", 59 | "", 60 | "", 61 | }, 62 | { 63 | "Fail to get resource", 64 | nil, 65 | errors.New(`fail to get resource`), 66 | func() *fake.FakeDynamicClient { 67 | client := fake.NewSimpleDynamicClient(k8sruntime.NewScheme()) 68 | client.PrependReactor("get", "tests", func(action k8stesting.Action) (bool, k8sruntime.Object, error) { 69 | return true, nil, errors.New("fail to get resource") 70 | }) 71 | return client 72 | }(), 73 | nil, 74 | "test/v1", 75 | "test", 76 | "testname", 77 | "testnamespace", 78 | }, 79 | { 80 | "Success, Deployment", 81 | &unstructured.Unstructured{ 82 | Object: map[string]interface{}{ 83 | "metadata": map[string]interface{}{ 84 | "name": "testname", 85 | "namespace": "testnamespace", 86 | }, 87 | "apiVersion": "apps/v1", 88 | "kind": "Deployment", 89 | }, 90 | }, 91 | nil, 92 | fake.NewSimpleDynamicClient(k8sruntime.NewScheme(), 93 | &unstructured.Unstructured{ 94 | Object: map[string]interface{}{ 95 | "apiVersion": "apps/v1", 96 | "kind": "Deployment", 97 | "metadata": map[string]interface{}{ 98 | "namespace": "testnamespace", 99 | "name": "testname", 100 | }, 101 | }, 102 | }, 103 | ), 104 | k8sruntime.DefaultUnstructuredConverter, 105 | "apps/v1", 106 | "Deployment", 107 | "testname", 108 | "testnamespace", 109 | }, 110 | { 111 | "Success, Argo Rollout", 112 | &unstructured.Unstructured{ 113 | Object: map[string]interface{}{ 114 | "metadata": map[string]interface{}{ 115 | "name": "testname", 116 | "namespace": "testnamespace", 117 | }, 118 | "apiVersion": "argoproj.io/v1alpha1", 119 | "kind": "Rollout", 120 | }, 121 | }, 122 | nil, 123 | fake.NewSimpleDynamicClient(k8sruntime.NewScheme(), 124 | &unstructured.Unstructured{ 125 | Object: map[string]interface{}{ 126 | "apiVersion": "argoproj.io/v1alpha1", 127 | "kind": "Rollout", 128 | "metadata": map[string]interface{}{ 129 | "namespace": "testnamespace", 130 | "name": "testname", 131 | }, 132 | }, 133 | }, 134 | ), 135 | k8sruntime.DefaultUnstructuredConverter, 136 | "argoproj.io/v1alpha1", 137 | "Rollout", 138 | "testname", 139 | "testnamespace", 140 | }, 141 | } 142 | 143 | for _, test := range tests { 144 | t.Run(test.description, func(t *testing.T) { 145 | scaler := &resourceclient.UnstructuredClient{ 146 | Dynamic: test.dynamic, 147 | } 148 | result, err := scaler.Get(test.apiVersion, test.kind, test.name, test.namespace) 149 | if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { 150 | t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) 151 | return 152 | } 153 | if !cmp.Equal(test.expected, result) { 154 | t.Errorf("metrics mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) 155 | } 156 | }) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /examples/cpureplicaprint/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "log" 22 | "path/filepath" 23 | "time" 24 | 25 | "github.com/jthomperoo/k8shorizmetrics/v4" 26 | "github.com/jthomperoo/k8shorizmetrics/v4/metricsclient" 27 | "github.com/jthomperoo/k8shorizmetrics/v4/podsclient" 28 | v2 "k8s.io/api/autoscaling/v2" 29 | corev1 "k8s.io/api/core/v1" 30 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 | "k8s.io/apimachinery/pkg/labels" 32 | "k8s.io/client-go/kubernetes" 33 | "k8s.io/client-go/tools/clientcmd" 34 | "k8s.io/client-go/util/homedir" 35 | ) 36 | 37 | const ( 38 | cpuInitializationPeriodSeconds = 300 39 | initialReadinessDelaySeconds = 30 40 | tolerance = 0.1 41 | namespace = "default" 42 | deploymentName = "php-apache" 43 | ) 44 | 45 | var targetAverageUtilization int32 = 50 46 | 47 | var podMatchSelector = labels.SelectorFromSet(labels.Set{ 48 | "run": "php-apache", 49 | }) 50 | 51 | func main() { 52 | clusterConfig, err := clientcmd.BuildConfigFromFlags("", filepath.Join(homedir.HomeDir(), ".kube", "config")) 53 | if err != nil { 54 | log.Fatalf("Fail to create out-of-cluster Kubernetes config: %s", err) 55 | } 56 | 57 | clientset, err := kubernetes.NewForConfig(clusterConfig) 58 | if err != nil { 59 | log.Fatalf("Fail to set up Kubernetes clientset: %s", err) 60 | } 61 | 62 | metricsclient := metricsclient.NewClient(clusterConfig, clientset.Discovery()) 63 | podsclient := &podsclient.OnDemandPodLister{ 64 | Clientset: clientset, 65 | } 66 | cpuInitializationPeriod := time.Duration(cpuInitializationPeriodSeconds) * time.Second 67 | initialReadinessDelay := time.Duration(initialReadinessDelaySeconds) * time.Second 68 | 69 | // Set up the metric gatherer, needs to be able to query metrics and pods with the clients provided, along with 70 | // config options 71 | gather := k8shorizmetrics.NewGatherer(metricsclient, podsclient, cpuInitializationPeriod, initialReadinessDelay) 72 | // Set up the evaluator, only needs to know the tolerance configuration value for determining replica counts 73 | evaluator := k8shorizmetrics.NewEvaluator(tolerance) 74 | 75 | // This is the metric spec, this targets the CPU resource metric, gathering utilization values and targeting 76 | // an average utilization of 50% 77 | // Equivalent to the following YAML: 78 | // metrics: 79 | // - type: Resource 80 | // resource: 81 | // name: cpu 82 | // target: 83 | // type: Utilization 84 | // averageUtilization: 50 85 | spec := v2.MetricSpec{ 86 | Type: v2.ResourceMetricSourceType, 87 | Resource: &v2.ResourceMetricSource{ 88 | Name: corev1.ResourceCPU, 89 | Target: v2.MetricTarget{ 90 | Type: v2.UtilizationMetricType, 91 | AverageUtilization: &targetAverageUtilization, 92 | }, 93 | }, 94 | } 95 | 96 | // Loop infinitely, wait 5 seconds between each loop 97 | for { 98 | time.Sleep(5 * time.Second) 99 | 100 | // Gather the metrics using the spec, targeting the namespace and pod selector defined above 101 | metric, err := gather.GatherSingleMetric(spec, namespace, podMatchSelector) 102 | if err != nil { 103 | log.Println(err) 104 | continue 105 | } 106 | 107 | log.Println("CPU metrics:") 108 | 109 | for pod, podmetric := range metric.Resource.PodMetricsInfo { 110 | actualCPU := podmetric.Value 111 | requestedCPU := metric.Resource.Requests[pod] 112 | log.Printf("Pod: %s, CPU usage: %dm (%0.2f%% of requested)\n", pod, actualCPU, float64(actualCPU)/float64(requestedCPU)*100.0) 113 | } 114 | 115 | // To find out the current replica count we can use the Kubernetes client-go client to get the scale sub 116 | // resource of the deployment which contains the current replica count 117 | scale, err := clientset.AppsV1().Deployments(namespace).GetScale(context.Background(), deploymentName, metav1.GetOptions{}) 118 | if err != nil { 119 | log.Printf("Failed to get scale resource for deployment '%s', err: %v", deploymentName, err) 120 | continue 121 | } 122 | 123 | currentReplicaCount := scale.Spec.Replicas 124 | 125 | // Calculate the target number of replicas that the HPA would scale to based on the metric provided, current 126 | // replicas, and the tolerance configuration value provided 127 | targetReplicaCount, err := evaluator.EvaluateSingleMetric(metric, scale.Spec.Replicas) 128 | if err != nil { 129 | log.Println(err) 130 | continue 131 | } 132 | 133 | if targetReplicaCount == currentReplicaCount { 134 | log.Printf("The Horizontal Pod Autoscaler would stay at %d replicas", targetReplicaCount) 135 | } else { 136 | log.Printf("The Horizontal Pod Autoscaler would scale from %d to %d replicas", currentReplicaCount, targetReplicaCount) 137 | } 138 | 139 | log.Println("----------") 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /internal/resource/evaluate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Modifications Copyright 2022 The K8sHorizMetrics Authors. 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | 21 | Modified to split up evaluations and metric gathering to work with the 22 | Custom Pod Autoscaler framework. 23 | Original source: 24 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/horizontal.go 25 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/replica_calculator.go 26 | */ 27 | 28 | package resource 29 | 30 | import ( 31 | "fmt" 32 | "math" 33 | 34 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/replicas" 35 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics" 36 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/podmetrics" 37 | "github.com/jthomperoo/k8shorizmetrics/v4/metricsclient" 38 | ) 39 | 40 | // Evaluate (resource) calculates a replica count evaluation, using the tolerance and calculater provided 41 | type Evaluate struct { 42 | Calculater replicas.Calculator 43 | } 44 | 45 | // Evaluate calculates an evaluation based on the metric provided and the current number of replicas 46 | func (e *Evaluate) Evaluate(currentReplicas int32, gatheredMetric *metrics.Metric, tolerance float64) (int32, error) { 47 | if gatheredMetric.Spec.Resource.Target.AverageValue != nil { 48 | replicaCount := e.Calculater.GetPlainMetricReplicaCount( 49 | gatheredMetric.Resource.PodMetricsInfo, 50 | currentReplicas, 51 | gatheredMetric.Spec.Resource.Target.AverageValue.MilliValue(), 52 | gatheredMetric.Resource.ReadyPodCount, 53 | gatheredMetric.Resource.MissingPods, 54 | gatheredMetric.Resource.IgnoredPods, 55 | ) 56 | return replicaCount, nil 57 | } 58 | 59 | if gatheredMetric.Spec.Resource.Target.AverageUtilization != nil { 60 | metrics := gatheredMetric.Resource.PodMetricsInfo 61 | requests := gatheredMetric.Resource.Requests 62 | targetUtilization := *gatheredMetric.Spec.Resource.Target.AverageUtilization 63 | ignoredPods := gatheredMetric.Resource.IgnoredPods 64 | missingPods := gatheredMetric.Resource.MissingPods 65 | readyPodCount := gatheredMetric.Resource.ReadyPodCount 66 | 67 | usageRatio, _, _, err := metricsclient.GetResourceUtilizationRatio(metrics, requests, targetUtilization) 68 | if err != nil { 69 | return 0, err 70 | } 71 | 72 | // usageRatio = SUM(pod metrics) / SUM(pod requests) / targetUtilization 73 | // usageRatio = averageUtilization / targetUtilization 74 | // usageRatio ~ 1.0 == no scale 75 | // usageRatio > 1.0 == scale up 76 | // usageRatio < 1.0 == scale down 77 | 78 | rebalanceIgnored := len(ignoredPods) > 0 && usageRatio > 1.0 79 | if !rebalanceIgnored && len(missingPods) == 0 { 80 | if math.Abs(1.0-usageRatio) <= tolerance { 81 | // return the current replicas if the change would be too small 82 | return currentReplicas, nil 83 | } 84 | targetReplicas := int32(math.Ceil(usageRatio * float64(readyPodCount))) 85 | // if we don't have any unready or missing pods, we can calculate the new replica count now 86 | return targetReplicas, nil 87 | } 88 | 89 | if len(missingPods) > 0 { 90 | if usageRatio < 1.0 { 91 | // on a scale-down, treat missing pods as using 100% of the resource request 92 | for podName := range missingPods { 93 | metrics[podName] = podmetrics.Metric{Value: requests[podName]} 94 | } 95 | } else if usageRatio > 1.0 { 96 | // on a scale-up, treat missing pods as using 0% of the resource request 97 | for podName := range missingPods { 98 | metrics[podName] = podmetrics.Metric{Value: 0} 99 | } 100 | } 101 | } 102 | 103 | if rebalanceIgnored { 104 | // on a scale-up, treat unready pods as using 0% of the resource request 105 | for podName := range ignoredPods { 106 | metrics[podName] = podmetrics.Metric{Value: 0} 107 | } 108 | } 109 | 110 | // re-run the utilization calculation with our new numbers 111 | newUsageRatio, _, _, err := metricsclient.GetResourceUtilizationRatio(metrics, requests, targetUtilization) 112 | if err != nil { 113 | // NOTE - Unsure if this can be triggered. 114 | return 0, err 115 | } 116 | 117 | if math.Abs(1.0-newUsageRatio) <= tolerance || (usageRatio < 1.0 && newUsageRatio > 1.0) || (usageRatio > 1.0 && newUsageRatio < 1.0) { 118 | // return the current replicas if the change would be too small, 119 | // or if the new usage ratio would cause a change in scale direction 120 | return currentReplicas, nil 121 | } 122 | 123 | // return the result, where the number of replicas considered is 124 | // however many replicas factored into our calculation 125 | targetReplicas := int32(math.Ceil(newUsageRatio * float64(len(metrics)))) 126 | return targetReplicas, nil 127 | } 128 | 129 | return 0, fmt.Errorf("invalid resource metric source: neither a utilization target nor a value target was set") 130 | } 131 | -------------------------------------------------------------------------------- /internal/fake/gather.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package fake 18 | 19 | import ( 20 | "time" 21 | 22 | externalmetrics "github.com/jthomperoo/k8shorizmetrics/v4/metrics/external" 23 | objectmetrics "github.com/jthomperoo/k8shorizmetrics/v4/metrics/object" 24 | podsmetrics "github.com/jthomperoo/k8shorizmetrics/v4/metrics/pods" 25 | resourcemetrics "github.com/jthomperoo/k8shorizmetrics/v4/metrics/resource" 26 | autoscalingv2 "k8s.io/api/autoscaling/v2" 27 | corev1 "k8s.io/api/core/v1" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "k8s.io/apimachinery/pkg/labels" 30 | ) 31 | 32 | // ExternalGatherer (fake) provides a way to insert functionality into a ExternalGatherer 33 | type ExternalGatherer struct { 34 | GatherReactor func(metricName, namespace string, metricSelector *metav1.LabelSelector, podSelector labels.Selector) (*externalmetrics.Metric, error) 35 | GatherPerPodReactor func(metricName, namespace string, metricSelector *metav1.LabelSelector) (*externalmetrics.Metric, error) 36 | } 37 | 38 | // Gather calls the fake ExternalGatherer function 39 | func (f *ExternalGatherer) Gather(metricName, namespace string, metricSelector *metav1.LabelSelector, 40 | podSelector labels.Selector) (*externalmetrics.Metric, error) { 41 | return f.GatherReactor(metricName, namespace, metricSelector, podSelector) 42 | } 43 | 44 | // GatherPerPod calls the fake ExternalGatherer function 45 | func (f *ExternalGatherer) GatherPerPod(metricName, namespace string, 46 | metricSelector *metav1.LabelSelector) (*externalmetrics.Metric, error) { 47 | return f.GatherPerPodReactor(metricName, namespace, metricSelector) 48 | } 49 | 50 | // ObjectGatherer (fake) provides a way to insert functionality into a ObjectGatherer 51 | type ObjectGatherer struct { 52 | GatherReactor func(metricName string, namespace string, objectRef *autoscalingv2.CrossVersionObjectReference, 53 | podSelector labels.Selector, metricSelector labels.Selector) (*objectmetrics.Metric, error) 54 | GatherPerPodReactor func(metricName string, namespace string, objectRef *autoscalingv2.CrossVersionObjectReference, 55 | metricSelector labels.Selector) (*objectmetrics.Metric, error) 56 | } 57 | 58 | // Gather calls the fake ObjectGatherer function 59 | func (f *ObjectGatherer) Gather(metricName string, namespace string, 60 | objectRef *autoscalingv2.CrossVersionObjectReference, podSelector labels.Selector, 61 | metricSelector labels.Selector) (*objectmetrics.Metric, error) { 62 | return f.GatherReactor(metricName, namespace, objectRef, podSelector, metricSelector) 63 | } 64 | 65 | // GatherPerPod calls the fake ObjectGatherer function 66 | func (f *ObjectGatherer) GatherPerPod(metricName string, namespace string, 67 | objectRef *autoscalingv2.CrossVersionObjectReference, 68 | metricSelector labels.Selector) (*objectmetrics.Metric, error) { 69 | return f.GatherPerPodReactor(metricName, namespace, objectRef, metricSelector) 70 | } 71 | 72 | // PodsGatherer (fake) provides a way to insert functionality into a PodsGatherer 73 | type PodsGatherer struct { 74 | GatherReactor func(metricName string, namespace string, podSelector labels.Selector, 75 | metricSelector labels.Selector) (*podsmetrics.Metric, error) 76 | } 77 | 78 | // Gather calls the fake PodsGatherer function 79 | func (f *PodsGatherer) Gather(metricName string, namespace string, podSelector labels.Selector, 80 | metricSelector labels.Selector) (*podsmetrics.Metric, error) { 81 | return f.GatherReactor(metricName, namespace, podSelector, metricSelector) 82 | } 83 | 84 | // ResourceGatherer (fake) provides a way to insert functionality into a ResourceGatherer 85 | type ResourceGatherer struct { 86 | GatherReactor func(resourceName corev1.ResourceName, namespace string, podSelector labels.Selector, 87 | cpuInitializationPeriod time.Duration, 88 | delayOfInitialReadinessStatus time.Duration) (*resourcemetrics.Metric, error) 89 | GatherRawReactor func(resourceName corev1.ResourceName, namespace string, podSelector labels.Selector, 90 | cpuInitializationPeriod time.Duration, 91 | delayOfInitialReadinessStatus time.Duration) (*resourcemetrics.Metric, error) 92 | } 93 | 94 | // Gather calls the fake ResourceGatherer function 95 | func (f *ResourceGatherer) Gather(resourceName corev1.ResourceName, namespace string, podSelector labels.Selector, 96 | cpuInitializationPeriod time.Duration, 97 | delayOfInitialReadinessStatus time.Duration) (*resourcemetrics.Metric, error) { 98 | return f.GatherReactor(resourceName, namespace, podSelector, cpuInitializationPeriod, delayOfInitialReadinessStatus) 99 | } 100 | 101 | // GatherRaw calls the fake ResourceGatherer function 102 | func (f *ResourceGatherer) GatherRaw(resourceName corev1.ResourceName, namespace string, podSelector labels.Selector, 103 | cpuInitializationPeriod time.Duration, 104 | delayOfInitialReadinessStatus time.Duration) (*resourcemetrics.Metric, error) { 105 | return f.GatherRawReactor(resourceName, namespace, podSelector, cpuInitializationPeriod, delayOfInitialReadinessStatus) 106 | } 107 | -------------------------------------------------------------------------------- /internal/replicas/replicas.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Modifications Copyright 2022 The K8sHorizMetrics Authors. 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | 21 | Modified to split up evaluations and metric gathering to work with the 22 | Custom Pod Autoscaler framework. 23 | Original source: 24 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/horizontal.go 25 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/replica_calculator.go 26 | */ 27 | 28 | // Package replicas provides utilities for getting replica counts from the K8s APIs. 29 | package replicas 30 | 31 | import ( 32 | "math" 33 | 34 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/podmetrics" 35 | "github.com/jthomperoo/k8shorizmetrics/v4/metricsclient" 36 | "k8s.io/apimachinery/pkg/util/sets" 37 | ) 38 | 39 | // Calculator is used to calculate replica counts 40 | type Calculator interface { 41 | GetUsageRatioReplicaCount(currentReplicas int32, usageRatio float64, readyPodCount int64) int32 42 | GetPlainMetricReplicaCount(metrics podmetrics.MetricsInfo, 43 | currentReplicas int32, 44 | targetUtilization int64, 45 | readyPodCount int64, 46 | missingPods, 47 | ignoredPods sets.String) int32 48 | } 49 | 50 | // ReplicaCalculator uses a tolerance provided to calculate replica counts for scaling up/down/remaining the same 51 | type ReplicaCalculator struct { 52 | Tolerance float64 53 | } 54 | 55 | // GetUsageRatioReplicaCount calculates the replica count based on the number of replicas, number of ready pods and the 56 | // usage ratio of the metric - providing a different value if beyond the tolerance 57 | func (r *ReplicaCalculator) GetUsageRatioReplicaCount(currentReplicas int32, usageRatio float64, readyPodCount int64) int32 { 58 | var replicaCount int32 59 | if currentReplicas != 0 { 60 | if math.Abs(1.0-usageRatio) <= r.Tolerance { 61 | // return the current replicas if the change would be too small 62 | return currentReplicas 63 | } 64 | replicaCount = int32(math.Ceil(usageRatio * float64(readyPodCount))) 65 | } else { 66 | // Scale to zero or n pods depending on usageRatio 67 | replicaCount = int32(math.Ceil(usageRatio)) 68 | } 69 | 70 | return replicaCount 71 | } 72 | 73 | // GetPlainMetricReplicaCount calculates the replica count based on the metrics of each pod and a target utilization, providing 74 | // a different replica count if the calculated usage ratio is beyond the tolerance 75 | func (r *ReplicaCalculator) GetPlainMetricReplicaCount(metrics podmetrics.MetricsInfo, 76 | currentReplicas int32, 77 | targetUtilization int64, 78 | readyPodCount int64, 79 | missingPods, 80 | ignoredPods sets.String) int32 { 81 | 82 | usageRatio, _ := metricsclient.GetMetricUtilizationRatio(metrics, targetUtilization) 83 | 84 | // usageRatio = SUM(pod metrics) / number of pods / targetUtilization 85 | // usageRatio = averageUtilization / targetUtilization 86 | // usageRatio ~ 1.0 == no scale 87 | // usageRatio > 1.0 == scale up 88 | // usageRatio < 1.0 == scale down 89 | 90 | rebalanceIgnored := len(ignoredPods) > 0 && usageRatio > 1.0 91 | 92 | if !rebalanceIgnored && len(missingPods) == 0 { 93 | if math.Abs(1.0-usageRatio) <= r.Tolerance { 94 | // return the current replicas if the change would be too small 95 | return currentReplicas 96 | } 97 | 98 | // if we don't have any unready or missing pods, we can calculate the new replica count now 99 | return int32(math.Ceil(usageRatio * float64(readyPodCount))) 100 | } 101 | 102 | if len(missingPods) > 0 { 103 | if usageRatio < 1.0 { 104 | // on a scale-down, treat missing pods as using 100% of the resource request 105 | for podName := range missingPods { 106 | metrics[podName] = podmetrics.Metric{Value: targetUtilization} 107 | } 108 | } else { 109 | // on a scale-up, treat missing pods as using 0% of the resource request 110 | for podName := range missingPods { 111 | metrics[podName] = podmetrics.Metric{Value: 0} 112 | } 113 | } 114 | } 115 | 116 | if rebalanceIgnored { 117 | // on a scale-up, treat unready pods as using 0% of the resource request 118 | for podName := range ignoredPods { 119 | metrics[podName] = podmetrics.Metric{Value: 0} 120 | } 121 | } 122 | 123 | // re-run the utilization calculation with our new numbers 124 | newUsageRatio, _ := metricsclient.GetMetricUtilizationRatio(metrics, targetUtilization) 125 | 126 | if math.Abs(1.0-newUsageRatio) <= r.Tolerance || (usageRatio < 1.0 && newUsageRatio > 1.0) || (usageRatio > 1.0 && newUsageRatio < 1.0) { 127 | // return the current replicas if the change would be too small, 128 | // or if the new usage ratio would cause a change in scale direction 129 | return currentReplicas 130 | } 131 | 132 | // return the result, where the number of replicas considered is 133 | // however many replicas factored into our calculation 134 | return int32(math.Ceil(newUsageRatio * float64(len(metrics)))) 135 | } 136 | -------------------------------------------------------------------------------- /internal/podutil/podutil.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Modifications Copyright 2022 The K8sHorizMetrics Authors. 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | 21 | Modified to split up evaluations and metric gathering to work with the 22 | Custom Pod Autoscaler framework. 23 | Original source: 24 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/horizontal.go 25 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/replica_calculator.go 26 | */ 27 | 28 | // Package podutil provides utilities for getting pod information from the K8s APIs. 29 | package podutil 30 | 31 | import ( 32 | "fmt" 33 | "time" 34 | 35 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/podmetrics" 36 | corev1 "k8s.io/api/core/v1" 37 | "k8s.io/apimachinery/pkg/labels" 38 | "k8s.io/apimachinery/pkg/util/sets" 39 | corelisters "k8s.io/client-go/listers/core/v1" 40 | ) 41 | 42 | // PodReadyCounter provides a way to count number of ready pods 43 | type PodReadyCounter interface { 44 | GetReadyPodsCount(namespace string, selector labels.Selector) (int64, error) 45 | } 46 | 47 | // PodReadyCount provides a way to count the number of ready pods using a pod lister 48 | type PodReadyCount struct { 49 | PodLister corelisters.PodLister 50 | } 51 | 52 | // GetReadyPodsCount returns the number of pods that are deemed 'ready' 53 | func (c *PodReadyCount) GetReadyPodsCount(namespace string, selector labels.Selector) (int64, error) { 54 | // Get pods 55 | podList, err := c.PodLister.Pods(namespace).List(selector) 56 | if err != nil { 57 | return 0, fmt.Errorf("unable to get pods while calculating replica count: %w", err) 58 | } 59 | 60 | // Count number of ready pods 61 | readyPodCount := int64(0) 62 | for _, pod := range podList { 63 | if pod.Status.Phase == corev1.PodRunning && isPodReady(pod) { 64 | readyPodCount++ 65 | } 66 | } 67 | 68 | return readyPodCount, nil 69 | } 70 | 71 | // GroupPods groups pods into ready, missing and ignored based on PodMetricsInfo and resource provided 72 | func GroupPods(pods []*corev1.Pod, metrics podmetrics.MetricsInfo, resource corev1.ResourceName, cpuInitializationPeriod, delayOfInitialReadinessStatus time.Duration) (readyPodCount int, ignoredPods sets.String, missingPods sets.String) { 73 | missingPods = sets.NewString() 74 | ignoredPods = sets.NewString() 75 | for _, pod := range pods { 76 | if pod.DeletionTimestamp != nil || pod.Status.Phase == corev1.PodFailed { 77 | continue 78 | } 79 | // Pending pods are ignored. 80 | if pod.Status.Phase == corev1.PodPending { 81 | ignoredPods.Insert(pod.Name) 82 | continue 83 | } 84 | // Pods missing metrics. 85 | metric, found := metrics[pod.Name] 86 | if !found { 87 | missingPods.Insert(pod.Name) 88 | continue 89 | } 90 | // Unready pods are ignored. 91 | if resource == corev1.ResourceCPU { 92 | var ignorePod bool 93 | _, condition := getPodCondition(pod.Status, corev1.PodReady) 94 | if condition == nil || pod.Status.StartTime == nil { 95 | ignorePod = true 96 | } else { 97 | // Pod still within possible initialisation period. 98 | if pod.Status.StartTime.Add(cpuInitializationPeriod).After(time.Now()) { 99 | // Ignore sample if pod is unready or one window of metric wasn't collected since last state transition. 100 | ignorePod = condition.Status == corev1.ConditionFalse || metric.Timestamp.Before(condition.LastTransitionTime.Time.Add(metric.Window)) 101 | } else { 102 | // Ignore metric if pod is unready and it has never been ready. 103 | ignorePod = condition.Status == corev1.ConditionFalse && pod.Status.StartTime.Add(delayOfInitialReadinessStatus).After(condition.LastTransitionTime.Time) 104 | } 105 | } 106 | if ignorePod { 107 | ignoredPods.Insert(pod.Name) 108 | continue 109 | } 110 | } 111 | readyPodCount++ 112 | } 113 | return 114 | } 115 | 116 | // CalculatePodRequests calculates pod resource requests for a slice of pods 117 | func CalculatePodRequests(pods []*corev1.Pod, resource corev1.ResourceName) (map[string]int64, error) { 118 | requests := make(map[string]int64, len(pods)) 119 | for _, pod := range pods { 120 | podSum := int64(0) 121 | for _, container := range pod.Spec.Containers { 122 | if containerRequest, ok := container.Resources.Requests[resource]; ok { 123 | podSum += containerRequest.MilliValue() 124 | } else { 125 | return nil, fmt.Errorf("missing request for %s", resource) 126 | } 127 | } 128 | requests[pod.Name] = podSum 129 | } 130 | return requests, nil 131 | } 132 | 133 | // RemoveMetricsForPods removes the pods provided from the PodMetricsInfo provided 134 | func RemoveMetricsForPods(metrics podmetrics.MetricsInfo, pods sets.String) { 135 | for _, pod := range pods.UnsortedList() { 136 | delete(metrics, pod) 137 | } 138 | } 139 | 140 | // IsPodReady returns true if a pod is ready; false otherwise. 141 | func isPodReady(pod *corev1.Pod) bool { 142 | _, condition := getPodCondition(pod.Status, corev1.PodReady) 143 | return condition != nil && condition.Status == corev1.ConditionTrue 144 | } 145 | 146 | // GetPodCondition extracts the provided condition from the given status and returns that. 147 | // Returns nil and -1 if the condition is not present, and the index of the located condition. 148 | func getPodCondition(status corev1.PodStatus, conditionType corev1.PodConditionType) (int, *corev1.PodCondition) { 149 | conditions := status.Conditions 150 | if conditions == nil { 151 | return -1, nil 152 | } 153 | for i := range conditions { 154 | if conditions[i].Type == conditionType { 155 | return i, &conditions[i] 156 | } 157 | } 158 | return -1, nil 159 | } 160 | -------------------------------------------------------------------------------- /internal/replicas/replicas_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Custom Pod Autoscaler Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package replicas_test 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/google/go-cmp/cmp" 23 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/replicas" 24 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/podmetrics" 25 | "k8s.io/apimachinery/pkg/util/sets" 26 | ) 27 | 28 | func TestReplicaCalculate_GetUsageRatioReplicaCount(t *testing.T) { 29 | var tests = []struct { 30 | description string 31 | expected int32 32 | tolerance float64 33 | currentReplicas int32 34 | usageRatio float64 35 | readyPodCount int64 36 | }{ 37 | { 38 | "No current replicas, scale to zero", 39 | 0, 40 | 0.1, 41 | 0, 42 | 0, 43 | 0, 44 | }, 45 | { 46 | "No current replicas, scale to 2", 47 | 2, 48 | 0.1, 49 | 0, 50 | 2, 51 | 0, 52 | }, 53 | { 54 | "3 current replicas, within tolerance, no scale", 55 | 3, 56 | 0.1, 57 | 3, 58 | 0.95, 59 | 3, 60 | }, 61 | { 62 | "3 current replicas, beyond tolerance, scale up", 63 | 5, 64 | 0.1, 65 | 3, 66 | 1.4, 67 | 3, 68 | }, 69 | { 70 | "3 current replicas, beyond tolerance, scale down", 71 | 1, 72 | 0.1, 73 | 3, 74 | 0.3, 75 | 3, 76 | }, 77 | } 78 | for _, test := range tests { 79 | t.Run(test.description, func(t *testing.T) { 80 | calc := replicas.ReplicaCalculator{ 81 | Tolerance: test.tolerance, 82 | } 83 | result := calc.GetUsageRatioReplicaCount(test.currentReplicas, test.usageRatio, test.readyPodCount) 84 | if !cmp.Equal(test.expected, result) { 85 | t.Errorf("replica mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) 86 | } 87 | }) 88 | } 89 | } 90 | 91 | func TestReplicaCalculate_GetPlainMetricReplicaCount(t *testing.T) { 92 | var tests = []struct { 93 | description string 94 | expected int32 95 | tolerance float64 96 | metrics podmetrics.MetricsInfo 97 | currentReplicas int32 98 | targetUtilization int64 99 | readyPodCount int64 100 | missingPods sets.String 101 | ignoredPods sets.String 102 | }{ 103 | { 104 | "No ignored pods, no missing pods, within tolerance, no scale change", 105 | 2, 106 | 0.1, 107 | podmetrics.MetricsInfo{ 108 | "pod-1": podmetrics.Metric{ 109 | Value: 50, 110 | }, 111 | "pod-2": podmetrics.Metric{ 112 | Value: 50, 113 | }, 114 | }, 115 | 2, 116 | 50, 117 | 2, 118 | sets.String{}, 119 | sets.String{}, 120 | }, 121 | { 122 | "No ignored pods, no missing pods, beyond tolerance, scale up", 123 | 4, 124 | 0.1, 125 | podmetrics.MetricsInfo{ 126 | "pod-1": podmetrics.Metric{ 127 | Value: 100, 128 | }, 129 | "pod-2": podmetrics.Metric{ 130 | Value: 100, 131 | }, 132 | }, 133 | 2, 134 | 50, 135 | 2, 136 | sets.String{}, 137 | sets.String{}, 138 | }, 139 | { 140 | "No ignored pods, no missing pods, beyond tolerance, scale down", 141 | 1, 142 | 0.1, 143 | podmetrics.MetricsInfo{ 144 | "pod-1": podmetrics.Metric{ 145 | Value: 25, 146 | }, 147 | "pod-2": podmetrics.Metric{ 148 | Value: 25, 149 | }, 150 | }, 151 | 2, 152 | 50, 153 | 2, 154 | sets.String{}, 155 | sets.String{}, 156 | }, 157 | { 158 | "No ignored pods, 2 missing pods, beyond tolerance, scale up", 159 | 8, 160 | 0.1, 161 | podmetrics.MetricsInfo{ 162 | "pod-1": podmetrics.Metric{ 163 | Value: 200, 164 | }, 165 | "pod-2": podmetrics.Metric{ 166 | Value: 200, 167 | }, 168 | }, 169 | 4, 170 | 50, 171 | 2, 172 | sets.String{ 173 | "missing-1": {}, 174 | "missing-2": {}, 175 | }, 176 | sets.String{}, 177 | }, 178 | { 179 | "No ignored pods, 2 missing pods, beyond tolerance, scale down", 180 | 3, 181 | 0.1, 182 | podmetrics.MetricsInfo{ 183 | "pod-1": podmetrics.Metric{ 184 | Value: 25, 185 | }, 186 | "pod-2": podmetrics.Metric{ 187 | Value: 25, 188 | }, 189 | }, 190 | 4, 191 | 50, 192 | 2, 193 | sets.String{ 194 | "missing-1": {}, 195 | "missing-2": {}, 196 | }, 197 | sets.String{}, 198 | }, 199 | { 200 | "2 ignored pods, 2 missing pods, beyond tolerance, scale up", 201 | 16, 202 | 0.1, 203 | podmetrics.MetricsInfo{ 204 | "pod-1": podmetrics.Metric{ 205 | Value: 400, 206 | }, 207 | "pod-2": podmetrics.Metric{ 208 | Value: 400, 209 | }, 210 | }, 211 | 6, 212 | 50, 213 | 2, 214 | sets.String{ 215 | "missing-1": {}, 216 | "missing-2": {}, 217 | }, 218 | sets.String{ 219 | "ignored-1": {}, 220 | "ignored-2": {}, 221 | }, 222 | }, 223 | { 224 | "2 ignored pods, 2 missing pods, within tolerance, no scale change", 225 | 6, 226 | 0.1, 227 | podmetrics.MetricsInfo{ 228 | "pod-1": podmetrics.Metric{ 229 | Value: 150, 230 | }, 231 | "pod-2": podmetrics.Metric{ 232 | Value: 150, 233 | }, 234 | }, 235 | 6, 236 | 50, 237 | 2, 238 | sets.String{ 239 | "missing-1": {}, 240 | "missing-2": {}, 241 | }, 242 | sets.String{ 243 | "ignored-1": {}, 244 | "ignored-2": {}, 245 | }, 246 | }, 247 | } 248 | for _, test := range tests { 249 | t.Run(test.description, func(t *testing.T) { 250 | calc := replicas.ReplicaCalculator{ 251 | Tolerance: test.tolerance, 252 | } 253 | result := calc.GetPlainMetricReplicaCount(test.metrics, test.currentReplicas, test.targetUtilization, test.readyPodCount, test.missingPods, test.ignoredPods) 254 | if !cmp.Equal(test.expected, result) { 255 | t.Errorf("replica mismatch (-want +got):\n%s", cmp.Diff(test.expected, result)) 256 | } 257 | }) 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/jthomperoo/k8shorizmetrics/workflows/main/badge.svg)](https://github.com/jthomperoo/k8shorizmetrics/actions) 2 | [![go.dev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/jthomperoo/k8shorizmetrics/v4) 3 | [![Go Report 4 | Card](https://goreportcard.com/badge/github.com/jthomperoo/k8shorizmetrics/v4)](https://goreportcard.com/report/github.com/jthomperoo/k8shorizmetrics/v4) 5 | [![License](https://img.shields.io/:license-apache-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) 6 | 7 | # k8shorizmetrics 8 | 9 | `k8shorizmetrics` is a library that provides the internal workings of the Kubernetes Horizontal Pod Autoscaler (HPA) 10 | wrapped up in a simple API. The project allows querying metrics just as the HPA does, and also running the calculations 11 | to work out the target replica count that the HPA does. 12 | 13 | ## Install 14 | 15 | ```bash 16 | go get -u github.com/jthomperoo/k8shorizmetrics/v4@v4.0.0 17 | ``` 18 | 19 | ## Features 20 | 21 | - Simple API, based directly on the code from the HPA, but detangled for ease of use. 22 | - Dependent only on versioned and public Kubernetes Golang modules, allows easy install without replace directives. 23 | - Splits the HPA into two parts, metric gathering and evaluation, only use what you need. 24 | - Allows insights into how the HPA makes decisions. 25 | - Supports scaling to and from 0. 26 | 27 | ## Quick Start 28 | 29 | The following is a simple program that can run inside a Kubernetes cluster that gets the CPU resource metrics for 30 | pods with the label `run: php-apache`. 31 | 32 | ```go 33 | package main 34 | 35 | import ( 36 | "log" 37 | "time" 38 | 39 | "github.com/jthomperoo/k8shorizmetrics/v4" 40 | "github.com/jthomperoo/k8shorizmetrics/v4/metricsclient" 41 | "github.com/jthomperoo/k8shorizmetrics/v4/podsclient" 42 | "k8s.io/api/autoscaling/v2" 43 | corev1 "k8s.io/api/core/v1" 44 | "k8s.io/apimachinery/pkg/labels" 45 | "k8s.io/client-go/kubernetes" 46 | "k8s.io/client-go/rest" 47 | ) 48 | 49 | func main() { 50 | // Kubernetes API setup 51 | clusterConfig, _ := rest.InClusterConfig() 52 | clientset, _ := kubernetes.NewForConfig(clusterConfig) 53 | // Metrics and pods clients setup 54 | metricsclient := metricsclient.NewClient(clusterConfig, clientset.Discovery()) 55 | podsclient := &podsclient.OnDemandPodLister{Clientset: clientset} 56 | // HPA configuration options 57 | cpuInitializationPeriod := time.Duration(300) * time.Second 58 | initialReadinessDelay := time.Duration(30) * time.Second 59 | 60 | // Setup gatherer 61 | gather := k8shorizmetrics.NewGatherer(metricsclient, podsclient, cpuInitializationPeriod, initialReadinessDelay) 62 | 63 | // Target resource values 64 | namespace := "default" 65 | podSelector := labels.SelectorFromSet(labels.Set{ 66 | "run": "php-apache", 67 | }) 68 | 69 | // Metric spec to gather, CPU resource utilization 70 | spec := v2.MetricSpec{ 71 | Type: v2.ResourceMetricSourceType, 72 | Resource: &v2.ResourceMetricSource{ 73 | Name: corev1.ResourceCPU, 74 | Target: v2.MetricTarget{ 75 | Type: v2.UtilizationMetricType, 76 | }, 77 | }, 78 | } 79 | 80 | metric, _ := gather.GatherSingleMetric(spec, namespace, podSelector) 81 | 82 | for pod, podmetric := range metric.Resource.PodMetricsInfo { 83 | actualCPU := podmetric.Value 84 | requestedCPU := metric.Resource.Requests[pod] 85 | log.Printf("Pod: %s, CPU usage: %dm (%0.2f%% of requested)\n", pod, actualCPU, float64(actualCPU)/float64(requestedCPU)*100.0) 86 | } 87 | } 88 | ``` 89 | 90 | ## Documentation 91 | 92 | See the [Go doc](https://pkg.go.dev/github.com/jthomperoo/k8shorizmetrics/v4). 93 | 94 | ## Migration 95 | 96 | This section explains how to migrate between versions of the library. 97 | 98 | ### From v1 to v2 99 | 100 | There are two changes you need to make to migrate from `v1` to `v2`: 101 | 102 | 1. Switch from using `k8s.io/api/autoscaling/v2beta2` to `k8s.io/api/autoscaling/v2`. 103 | 2. Switch from using `github.com/jthomperoo/k8shorizmetrics` to `github.com/jthomperoo/k8shorizmetrics/v2`. 104 | 105 | ### From v2 to v3 106 | 107 | The breaking changes introduced by `v3` are: 108 | 109 | - Gather now returns the `GathererMultiMetricError` error type if any of the metrics fail to gather. This error is 110 | returned for partial errors, meaning some metrics gathered successfully and others did not. If this partial error 111 | occurs the `GathererMultiMetricError` error will have the `Partial` property set to `true`. This can be checked for 112 | using `errors.As`. 113 | - Evaluate now returns the `EvaluatorMultiMetricError` error type if any of the metrics fail to 114 | evaluate. This error is returned for partial errors, meaning some metrics evaluted successfully and others did not. 115 | If this partial error occurs the `EvaluatorMultiMetricError` error will have the `Partial` property set to `true`. This 116 | can be checked for using `errors.As`. 117 | 118 | To update to `v3` you will need to update all references in your code that refer to 119 | `github.com/jthomperoo/k8shorizmetrics/v2` to use `github.com/jthomperoo/k8shorizmetrics/v3`. 120 | 121 | If you want the behaviour to stay the same and to swallow partial errors you can use code like this: 122 | 123 | ```go 124 | metrics, err := gather.Gather(specs, namespace, podMatchSelector) 125 | if err != nil { 126 | gatherErr := &k8shorizmetrics.GathererMultiMetricError{} 127 | if !errors.As(err, &gatherErr) { 128 | log.Fatal(err) 129 | } 130 | 131 | if !gatherErr.Partial { 132 | log.Fatal(err) 133 | } 134 | 135 | // Not a partial error, just continue as normal 136 | } 137 | ``` 138 | 139 | You can use similar code for the `Evaluate` method of the `Evaluater`. 140 | 141 | ### From v3 to v4 142 | 143 | To update to `v4` you will need to update all references in your code that refer to 144 | `github.com/jthomperoo/k8shorizmetrics/v3` to use `github.com/jthomperoo/k8shorizmetrics/v4`. 145 | 146 | The only behaviour change is around serialisation into JSON. Fields are now serialised using camel case rather 147 | than snake case to match the Kubernetes conventions. 148 | 149 | If you are relying on JSON serialised values you need to use camel case now. For example the Resource Metric field 150 | `PodMetricsInfo` is now serialised as `podMetricsInfo` ratherthan `pod_metrics_info`. 151 | 152 | ## Examples 153 | 154 | See the [examples directory](./examples/) for some examples, [cpuprint](./examples/cpuprint/) is a good start. 155 | 156 | ## Developing and Contributing 157 | 158 | See the [contribution guidelines](CONTRIBUTING.md) and [code of conduct](CODE_OF_CONDUCT.md). 159 | -------------------------------------------------------------------------------- /internal/pods/gather_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package pods_test 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | "time" 23 | 24 | "github.com/google/go-cmp/cmp" 25 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/fake" 26 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/pods" 27 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/podmetrics" 28 | podsmetric "github.com/jthomperoo/k8shorizmetrics/v4/metrics/pods" 29 | metricsclient "github.com/jthomperoo/k8shorizmetrics/v4/metricsclient" 30 | v1 "k8s.io/api/core/v1" 31 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 | "k8s.io/apimachinery/pkg/labels" 33 | "k8s.io/apimachinery/pkg/util/sets" 34 | corelisters "k8s.io/client-go/listers/core/v1" 35 | ) 36 | 37 | func TestGather(t *testing.T) { 38 | equateErrorMessage := cmp.Comparer(func(x, y error) bool { 39 | if x == nil || y == nil { 40 | return x == nil && y == nil 41 | } 42 | return x.Error() == y.Error() 43 | }) 44 | 45 | var tests = []struct { 46 | description string 47 | expected *podsmetric.Metric 48 | expectedErr error 49 | metricsclient metricsclient.Client 50 | podLister corelisters.PodLister 51 | metricName string 52 | namespace string 53 | selector labels.Selector 54 | metricSelector labels.Selector 55 | }{ 56 | { 57 | "Fail to get metric", 58 | nil, 59 | errors.New("unable to get metric test-metric: fail to get metric"), 60 | &fake.MetricsClient{ 61 | GetRawMetricReactor: func(metricName, namespace string, selector, metricSelector labels.Selector) (podmetrics.MetricsInfo, time.Time, error) { 62 | return nil, time.Time{}, errors.New("fail to get metric") 63 | }, 64 | }, 65 | nil, 66 | "test-metric", 67 | "test-namespace", 68 | nil, 69 | nil, 70 | }, 71 | { 72 | "Fail to get pods", 73 | nil, 74 | errors.New("unable to get pods while calculating replica count: fail to get pods"), 75 | &fake.MetricsClient{ 76 | GetRawMetricReactor: func(metricName, namespace string, selector, metricSelector labels.Selector) (podmetrics.MetricsInfo, time.Time, error) { 77 | return nil, time.Time{}, nil 78 | }, 79 | }, 80 | &fake.PodLister{ 81 | PodsReactor: func(namespace string) corelisters.PodNamespaceLister { 82 | return &fake.PodNamespaceLister{ 83 | ListReactor: func(selector labels.Selector) (ret []*v1.Pod, err error) { 84 | return nil, errors.New("fail to get pods") 85 | }, 86 | } 87 | }, 88 | }, 89 | "test-metric", 90 | "test-namespace", 91 | nil, 92 | nil, 93 | }, 94 | { 95 | "No pods success", 96 | &podsmetric.Metric{ 97 | ReadyPodCount: 0, 98 | TotalPods: 0, 99 | Timestamp: time.Time{}, 100 | }, 101 | nil, 102 | &fake.MetricsClient{ 103 | GetRawMetricReactor: func(metricName, namespace string, selector, metricSelector labels.Selector) (podmetrics.MetricsInfo, time.Time, error) { 104 | return podmetrics.MetricsInfo{ 105 | "test-pod": podmetrics.Metric{}, 106 | }, time.Time{}, nil 107 | }, 108 | }, 109 | &fake.PodLister{ 110 | PodsReactor: func(namespace string) corelisters.PodNamespaceLister { 111 | return &fake.PodNamespaceLister{ 112 | ListReactor: func(selector labels.Selector) (ret []*v1.Pod, err error) { 113 | return []*v1.Pod{}, nil 114 | }, 115 | } 116 | }, 117 | }, 118 | "test-metric", 119 | "test-namespace", 120 | nil, 121 | nil, 122 | }, 123 | { 124 | "3 ready, 2 missing pods success", 125 | &podsmetric.Metric{ 126 | TotalPods: 5, 127 | ReadyPodCount: 3, 128 | MissingPods: sets.String{ 129 | "missing-pod-1": {}, 130 | "missing-pod-2": {}, 131 | }, 132 | PodMetricsInfo: podmetrics.MetricsInfo{ 133 | "ready-pod-1": podmetrics.Metric{}, 134 | "ready-pod-2": podmetrics.Metric{}, 135 | "ready-pod-3": podmetrics.Metric{}, 136 | }, 137 | }, 138 | nil, 139 | &fake.MetricsClient{ 140 | GetRawMetricReactor: func(metricName, namespace string, selector, metricSelector labels.Selector) (podmetrics.MetricsInfo, time.Time, error) { 141 | return podmetrics.MetricsInfo{ 142 | "ready-pod-1": podmetrics.Metric{}, 143 | "ready-pod-2": podmetrics.Metric{}, 144 | "ready-pod-3": podmetrics.Metric{}, 145 | }, time.Time{}, nil 146 | }, 147 | }, 148 | &fake.PodLister{ 149 | PodsReactor: func(namespace string) corelisters.PodNamespaceLister { 150 | return &fake.PodNamespaceLister{ 151 | ListReactor: func(selector labels.Selector) (ret []*v1.Pod, err error) { 152 | return []*v1.Pod{ 153 | { 154 | ObjectMeta: metav1.ObjectMeta{ 155 | Name: "ready-pod-1", 156 | }, 157 | }, 158 | { 159 | ObjectMeta: metav1.ObjectMeta{ 160 | Name: "ready-pod-2", 161 | }, 162 | }, 163 | { 164 | ObjectMeta: metav1.ObjectMeta{ 165 | Name: "ready-pod-3", 166 | }, 167 | }, 168 | { 169 | ObjectMeta: metav1.ObjectMeta{ 170 | Name: "missing-pod-1", 171 | }, 172 | }, 173 | { 174 | ObjectMeta: metav1.ObjectMeta{ 175 | Name: "missing-pod-2", 176 | }, 177 | }, 178 | }, nil 179 | }, 180 | } 181 | }, 182 | }, 183 | "test-metric", 184 | "test-namespace", 185 | nil, 186 | nil, 187 | }, 188 | } 189 | for _, test := range tests { 190 | t.Run(test.description, func(t *testing.T) { 191 | gatherer := &pods.Gather{ 192 | MetricsClient: test.metricsclient, 193 | PodLister: test.podLister, 194 | } 195 | metric, err := gatherer.Gather(test.metricName, test.namespace, test.selector, test.metricSelector) 196 | if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { 197 | t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) 198 | return 199 | } 200 | if !cmp.Equal(test.expected, metric) { 201 | t.Errorf("metrics mismatch (-want +got):\n%s", cmp.Diff(test.expected, metric)) 202 | } 203 | }) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /examples/cpuandmemoryreplicaprint/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "log" 23 | "path/filepath" 24 | "time" 25 | 26 | "github.com/jthomperoo/k8shorizmetrics/v4" 27 | "github.com/jthomperoo/k8shorizmetrics/v4/metricsclient" 28 | "github.com/jthomperoo/k8shorizmetrics/v4/podsclient" 29 | v2 "k8s.io/api/autoscaling/v2" 30 | corev1 "k8s.io/api/core/v1" 31 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 | "k8s.io/apimachinery/pkg/labels" 33 | "k8s.io/client-go/kubernetes" 34 | "k8s.io/client-go/tools/clientcmd" 35 | "k8s.io/client-go/util/homedir" 36 | ) 37 | 38 | const ( 39 | cpuInitializationPeriodSeconds = 300 40 | initialReadinessDelaySeconds = 30 41 | tolerance = 0.1 42 | namespace = "default" 43 | deploymentName = "php-apache" 44 | ) 45 | 46 | var targetAverageUtilization int32 = 50 47 | 48 | var podMatchSelector = labels.SelectorFromSet(labels.Set{ 49 | "run": "php-apache", 50 | }) 51 | 52 | func main() { 53 | clusterConfig, err := clientcmd.BuildConfigFromFlags("", filepath.Join(homedir.HomeDir(), ".kube", "config")) 54 | if err != nil { 55 | log.Fatalf("Fail to create out-of-cluster Kubernetes config: %s", err) 56 | } 57 | 58 | clientset, err := kubernetes.NewForConfig(clusterConfig) 59 | if err != nil { 60 | log.Fatalf("Fail to set up Kubernetes clientset: %s", err) 61 | } 62 | 63 | metricsclient := metricsclient.NewClient(clusterConfig, clientset.Discovery()) 64 | podsclient := &podsclient.OnDemandPodLister{ 65 | Clientset: clientset, 66 | } 67 | cpuInitializationPeriod := time.Duration(cpuInitializationPeriodSeconds) * time.Second 68 | initialReadinessDelay := time.Duration(initialReadinessDelaySeconds) * time.Second 69 | 70 | // Set up the metric gatherer, needs to be able to query metrics and pods with the clients provided, along with 71 | // config options 72 | gather := k8shorizmetrics.NewGatherer(metricsclient, podsclient, cpuInitializationPeriod, initialReadinessDelay) 73 | // Set up the evaluator, only needs to know the tolerance configuration value for determining replica counts 74 | evaluator := k8shorizmetrics.NewEvaluator(tolerance) 75 | 76 | // This is the CPU metric spec, this targets the CPU resource metric, gathering utilization values and targeting 77 | // an average utilization of 50% 78 | // Equivalent to the following YAML: 79 | // metrics: 80 | // - type: Resource 81 | // resource: 82 | // name: cpu 83 | // target: 84 | // type: Utilization 85 | // averageUtilization: 50 86 | cpuSpec := v2.MetricSpec{ 87 | Type: v2.ResourceMetricSourceType, 88 | Resource: &v2.ResourceMetricSource{ 89 | Name: corev1.ResourceCPU, 90 | Target: v2.MetricTarget{ 91 | Type: v2.UtilizationMetricType, 92 | AverageUtilization: &targetAverageUtilization, 93 | }, 94 | }, 95 | } 96 | 97 | // This is the memory metric spec, this targets the memory resource metric, gathering utilization values and 98 | // targeting an average utilization of 50% 99 | // Equivalent to the following YAML: 100 | // metrics: 101 | // - type: Resource 102 | // resource: 103 | // name: memory 104 | // target: 105 | // type: Utilization 106 | // averageUtilization: 50 107 | memorySpec := v2.MetricSpec{ 108 | Type: v2.ResourceMetricSourceType, 109 | Resource: &v2.ResourceMetricSource{ 110 | Name: corev1.ResourceMemory, 111 | Target: v2.MetricTarget{ 112 | Type: v2.UtilizationMetricType, 113 | AverageUtilization: &targetAverageUtilization, 114 | }, 115 | }, 116 | } 117 | 118 | // Loop infinitely, wait 5 seconds between each loop 119 | for { 120 | time.Sleep(5 * time.Second) 121 | 122 | specs := []v2.MetricSpec{cpuSpec, memorySpec} 123 | 124 | // Gather the metrics using the specs, targeting the namespace and pod selector defined above 125 | metrics, err := gather.Gather(specs, namespace, podMatchSelector) 126 | if err != nil { 127 | var gatherError *k8shorizmetrics.GathererMultiMetricError 128 | if errors.As(err, &gatherError) { 129 | for i, partialErr := range gatherError.Errors { 130 | log.Printf("partial error %d/%d: %s", i+1, len(gatherError.Errors), partialErr) 131 | } 132 | 133 | if !gatherError.Partial { 134 | continue 135 | } 136 | 137 | log.Printf("%d/%d metrics were gathered successfully, continuing to evaluate with available metrics", len(metrics), len(specs)) 138 | } else { 139 | log.Println(err) 140 | continue 141 | } 142 | } 143 | 144 | log.Println("Pod Metrics:") 145 | 146 | for _, metric := range metrics { 147 | for pod, podmetric := range metric.Resource.PodMetricsInfo { 148 | actual := podmetric.Value 149 | requested := metric.Resource.Requests[pod] 150 | log.Printf("Pod: %s, %s usage: %d (%0.2f%% of requested)\n", pod, &metric.Spec.Resource.Name, actual, float64(actual)/float64(requested)*100.0) 151 | } 152 | } 153 | 154 | // To find out the current replica count we can use the Kubernetes client-go client to get the scale sub 155 | // resource of the deployment which contains the current replica count 156 | scale, err := clientset.AppsV1().Deployments(namespace).GetScale(context.Background(), deploymentName, metav1.GetOptions{}) 157 | if err != nil { 158 | log.Printf("Failed to get scale resource for deployment '%s', err: %v", deploymentName, err) 159 | continue 160 | } 161 | 162 | currentReplicaCount := scale.Spec.Replicas 163 | 164 | // Calculate the target number of replicas that the HPA would scale to based on the metrics provided and current 165 | // replicas 166 | targetReplicaCount, err := evaluator.Evaluate(metrics, scale.Spec.Replicas) 167 | if err != nil { 168 | var evaluatorError *k8shorizmetrics.EvaluatorMultiMetricError 169 | if errors.As(err, &evaluatorError) { 170 | for i, partialErr := range evaluatorError.Errors { 171 | log.Printf("partial error %d/%d: %s", i+1, len(evaluatorError.Errors), partialErr) 172 | } 173 | 174 | if !evaluatorError.Partial { 175 | continue 176 | } 177 | 178 | log.Printf("%d/%d metrics were evaluated successfully, printing the expected evaluation using the available metrics", len(metrics), len(specs)) 179 | } else { 180 | log.Println(err) 181 | continue 182 | } 183 | } 184 | 185 | if targetReplicaCount == currentReplicaCount { 186 | log.Printf("The Horizontal Pod Autoscaler would stay at %d replicas", targetReplicaCount) 187 | } else { 188 | log.Printf("The Horizontal Pod Autoscaler would scale from %d to %d replicas", currentReplicaCount, targetReplicaCount) 189 | } 190 | 191 | log.Println("----------") 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /evaluate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Modifications Copyright 2024 The K8sHorizMetrics Authors. 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | 21 | Modified to split up evaluations and metric gathering to work with the 22 | Custom Pod Autoscaler framework. 23 | Original source: 24 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/horizontal.go 25 | https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/podautoscaler/replica_calculator.go 26 | */ 27 | 28 | package k8shorizmetrics 29 | 30 | import ( 31 | "fmt" 32 | 33 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/external" 34 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/object" 35 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/pods" 36 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/replicas" 37 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/resource" 38 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics" 39 | autoscalingv2 "k8s.io/api/autoscaling/v2" 40 | ) 41 | 42 | // EvaluatorMultiMetricError occurs when evaluating multiple metrics, if any metric fails to be evaluated this error 43 | // will be returned which contains all of the individual errors in the 'Errors' slice, if some metrics 44 | // were evaluated successfully the error will have the 'Partial' property set to true. 45 | type EvaluatorMultiMetricError struct { 46 | Partial bool 47 | Errors []error 48 | } 49 | 50 | func (e *EvaluatorMultiMetricError) Error() string { 51 | return fmt.Sprintf("evaluator multi metric error: %d errors, first error is %s", len(e.Errors), e.Errors[0]) 52 | } 53 | 54 | // ExternalEvaluater produces a replica count based on an external metric provided 55 | type ExternalEvaluater interface { 56 | Evaluate(currentReplicas int32, gatheredMetric *metrics.Metric, tolerance float64) (int32, error) 57 | } 58 | 59 | // ObjectEvaluater produces a replica count based on an object metric provided 60 | type ObjectEvaluater interface { 61 | Evaluate(currentReplicas int32, gatheredMetric *metrics.Metric, tolerance float64) (int32, error) 62 | } 63 | 64 | // PodsEvaluater produces a replica count based on a pods metric provided 65 | type PodsEvaluater interface { 66 | Evaluate(currentReplicas int32, gatheredMetric *metrics.Metric) int32 67 | } 68 | 69 | // ResourceEvaluater produces an evaluation based on a resource metric provided 70 | type ResourceEvaluater interface { 71 | Evaluate(currentReplicas int32, gatheredMetric *metrics.Metric, tolerance float64) (int32, error) 72 | } 73 | 74 | // Evaluator provides functionality for deciding how many replicas a resource should have based on provided metrics. 75 | type Evaluator struct { 76 | External ExternalEvaluater 77 | Object ObjectEvaluater 78 | Pods PodsEvaluater 79 | Resource ResourceEvaluater 80 | Tolerance float64 81 | } 82 | 83 | // NewEvaluator sets up an evaluate that can process external, object, pod and resource metrics 84 | func NewEvaluator(tolerance float64) *Evaluator { 85 | calculate := &replicas.ReplicaCalculator{ 86 | Tolerance: tolerance, 87 | } 88 | return &Evaluator{ 89 | External: &external.Evaluate{ 90 | Calculater: calculate, 91 | }, 92 | Object: &object.Evaluate{ 93 | Calculater: calculate, 94 | }, 95 | Pods: &pods.Evaluate{ 96 | Calculater: calculate, 97 | }, 98 | Resource: &resource.Evaluate{ 99 | Calculater: calculate, 100 | }, 101 | } 102 | } 103 | 104 | // Evaluate returns the target replica count for an array of multiple metrics 105 | // If an error occurs evaluating any metric this will return a EvaluatorMultiMetricError. If a partial error occurs, 106 | // meaning some metrics were evaluated successfully and others failed, the 'Partial' property of this error will be 107 | // set to true. 108 | func (e *Evaluator) Evaluate(gatheredMetrics []*metrics.Metric, currentReplicas int32) (int32, error) { 109 | return e.EvaluateWithOptions(gatheredMetrics, currentReplicas, e.Tolerance) 110 | } 111 | 112 | // EvaluateWithOptions returns the target replica count for an array of multiple metrics with provided options 113 | // If an error occurs evaluating any metric this will return a EvaluatorMultiMetricError. If a partial error occurs, 114 | // meaning some metrics were evaluated successfully and others failed, the 'Partial' property of this error will be 115 | // set to true. 116 | func (e *Evaluator) EvaluateWithOptions(gatheredMetrics []*metrics.Metric, currentReplicas int32, 117 | tolerance float64) (int32, error) { 118 | var evaluation int32 119 | var evaluationErrors []error 120 | 121 | for i, gatheredMetric := range gatheredMetrics { 122 | proposedEvaluation, err := e.EvaluateSingleMetricWithOptions(gatheredMetric, currentReplicas, tolerance) 123 | if err != nil { 124 | evaluationErrors = append(evaluationErrors, err) 125 | continue 126 | } 127 | 128 | if i == 0 { 129 | evaluation = proposedEvaluation 130 | } 131 | 132 | // Multiple evaluations, take the highest replica count 133 | if proposedEvaluation > evaluation { 134 | evaluation = proposedEvaluation 135 | } 136 | } 137 | 138 | if len(evaluationErrors) > 0 { 139 | partial := len(evaluationErrors) < len(gatheredMetrics) 140 | if partial { 141 | return evaluation, &EvaluatorMultiMetricError{ 142 | Partial: partial, 143 | Errors: evaluationErrors, 144 | } 145 | } 146 | 147 | return 0, &EvaluatorMultiMetricError{ 148 | Partial: partial, 149 | Errors: evaluationErrors, 150 | } 151 | } 152 | 153 | return evaluation, nil 154 | } 155 | 156 | // EvaluateSingleMetric returns the target replica count for a single metrics 157 | func (e *Evaluator) EvaluateSingleMetric(gatheredMetric *metrics.Metric, currentReplicas int32) (int32, error) { 158 | return e.EvaluateSingleMetricWithOptions(gatheredMetric, currentReplicas, e.Tolerance) 159 | } 160 | 161 | // EvaluateSingleMetricWithOptions returns the target replica count for a single metrics with provided options 162 | func (e *Evaluator) EvaluateSingleMetricWithOptions(gatheredMetric *metrics.Metric, currentReplicas int32, 163 | tolerance float64) (int32, error) { 164 | switch gatheredMetric.Spec.Type { 165 | case autoscalingv2.ObjectMetricSourceType: 166 | return e.Object.Evaluate(currentReplicas, gatheredMetric, tolerance) 167 | case autoscalingv2.PodsMetricSourceType: 168 | return e.Pods.Evaluate(currentReplicas, gatheredMetric), nil 169 | case autoscalingv2.ResourceMetricSourceType: 170 | return e.Resource.Evaluate(currentReplicas, gatheredMetric, tolerance) 171 | case autoscalingv2.ExternalMetricSourceType: 172 | return e.External.Evaluate(currentReplicas, gatheredMetric, tolerance) 173 | default: 174 | return 0, fmt.Errorf("unknown metric source type %q", string(gatheredMetric.Spec.Type)) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /internal/object/gather_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package object_test 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | "time" 23 | 24 | "github.com/google/go-cmp/cmp" 25 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/fake" 26 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/object" 27 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/podutil" 28 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/testutil" 29 | objectmetric "github.com/jthomperoo/k8shorizmetrics/v4/metrics/object" 30 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/value" 31 | metricsclient "github.com/jthomperoo/k8shorizmetrics/v4/metricsclient" 32 | autoscalingv2 "k8s.io/api/autoscaling/v2" 33 | "k8s.io/apimachinery/pkg/labels" 34 | ) 35 | 36 | func TestGather(t *testing.T) { 37 | equateErrorMessage := cmp.Comparer(func(x, y error) bool { 38 | if x == nil || y == nil { 39 | return x == nil && y == nil 40 | } 41 | return x.Error() == y.Error() 42 | }) 43 | 44 | var tests = []struct { 45 | description string 46 | expected *objectmetric.Metric 47 | expectedErr error 48 | metricsclient metricsclient.Client 49 | podReadyCounter podutil.PodReadyCounter 50 | metricName string 51 | namespace string 52 | objectRef *autoscalingv2.CrossVersionObjectReference 53 | selector labels.Selector 54 | metricSelector labels.Selector 55 | }{ 56 | { 57 | "Fail to get metric", 58 | nil, 59 | errors.New("unable to get metric test-metric: on test-namespace : fail to get metric"), 60 | &fake.MetricsClient{ 61 | GetObjectMetricReactor: func(metricName string, namespace string, objectRef *autoscalingv2.CrossVersionObjectReference, metricSelector labels.Selector) (int64, time.Time, error) { 62 | return 0, time.Time{}, errors.New("fail to get metric") 63 | }, 64 | }, 65 | nil, 66 | "test-metric", 67 | "test-namespace", 68 | &autoscalingv2.CrossVersionObjectReference{}, 69 | nil, 70 | nil, 71 | }, 72 | { 73 | "Fail to get ready pods", 74 | nil, 75 | errors.New("unable to calculate ready pods: fail to get ready pods"), 76 | &fake.MetricsClient{ 77 | GetObjectMetricReactor: func(metricName string, namespace string, objectRef *autoscalingv2.CrossVersionObjectReference, metricSelector labels.Selector) (int64, time.Time, error) { 78 | return 0, time.Time{}, nil 79 | }, 80 | }, 81 | &fake.PodReadyCounter{ 82 | GetReadyPodsCountReactor: func(namespace string, selector labels.Selector) (int64, error) { 83 | return 0, errors.New("fail to get ready pods") 84 | }, 85 | }, 86 | "test-metric", 87 | "test-namespace", 88 | &autoscalingv2.CrossVersionObjectReference{}, 89 | nil, 90 | nil, 91 | }, 92 | { 93 | "Success", 94 | &objectmetric.Metric{ 95 | Current: value.MetricValue{ 96 | Value: testutil.Int64Ptr(5), 97 | }, 98 | ReadyPodCount: testutil.Int64Ptr(2), 99 | }, 100 | nil, 101 | &fake.MetricsClient{ 102 | GetObjectMetricReactor: func(metricName string, namespace string, objectRef *autoscalingv2.CrossVersionObjectReference, metricSelector labels.Selector) (int64, time.Time, error) { 103 | return 5, time.Time{}, nil 104 | }, 105 | }, 106 | &fake.PodReadyCounter{ 107 | GetReadyPodsCountReactor: func(namespace string, selector labels.Selector) (int64, error) { 108 | return 2, nil 109 | }, 110 | }, 111 | "test-metric", 112 | "test-namespace", 113 | &autoscalingv2.CrossVersionObjectReference{}, 114 | nil, 115 | nil, 116 | }, 117 | } 118 | for _, test := range tests { 119 | t.Run(test.description, func(t *testing.T) { 120 | gatherer := &object.Gather{ 121 | MetricsClient: test.metricsclient, 122 | PodReadyCounter: test.podReadyCounter, 123 | } 124 | metric, err := gatherer.Gather(test.metricName, test.namespace, test.objectRef, test.selector, test.metricSelector) 125 | if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { 126 | t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) 127 | return 128 | } 129 | if !cmp.Equal(test.expected, metric) { 130 | t.Errorf("metrics mismatch (-want +got):\n%s", cmp.Diff(test.expected, metric)) 131 | } 132 | }) 133 | } 134 | } 135 | 136 | func TestGatherPerPod(t *testing.T) { 137 | equateErrorMessage := cmp.Comparer(func(x, y error) bool { 138 | if x == nil || y == nil { 139 | return x == nil && y == nil 140 | } 141 | return x.Error() == y.Error() 142 | }) 143 | 144 | var tests = []struct { 145 | description string 146 | expected *objectmetric.Metric 147 | expectedErr error 148 | metricsclient metricsclient.Client 149 | podReadyCounter podutil.PodReadyCounter 150 | metricName string 151 | namespace string 152 | objectRef *autoscalingv2.CrossVersionObjectReference 153 | metricSelector labels.Selector 154 | }{ 155 | { 156 | "Fail to get metric", 157 | nil, 158 | errors.New("unable to get metric test-metric: on test-namespace /fail to get metric"), 159 | &fake.MetricsClient{ 160 | GetObjectMetricReactor: func(metricName string, namespace string, objectRef *autoscalingv2.CrossVersionObjectReference, metricSelector labels.Selector) (int64, time.Time, error) { 161 | return 0, time.Time{}, errors.New("fail to get metric") 162 | }, 163 | }, 164 | nil, 165 | "test-metric", 166 | "test-namespace", 167 | &autoscalingv2.CrossVersionObjectReference{}, 168 | nil, 169 | }, 170 | { 171 | "Success", 172 | &objectmetric.Metric{ 173 | Current: value.MetricValue{ 174 | AverageValue: testutil.Int64Ptr(5), 175 | }, 176 | }, 177 | nil, 178 | &fake.MetricsClient{ 179 | GetObjectMetricReactor: func(metricName string, namespace string, objectRef *autoscalingv2.CrossVersionObjectReference, metricSelector labels.Selector) (int64, time.Time, error) { 180 | return 5, time.Time{}, nil 181 | }, 182 | }, 183 | nil, 184 | "test-metric", 185 | "test-namespace", 186 | &autoscalingv2.CrossVersionObjectReference{}, 187 | nil, 188 | }, 189 | } 190 | for _, test := range tests { 191 | t.Run(test.description, func(t *testing.T) { 192 | gatherer := &object.Gather{ 193 | MetricsClient: test.metricsclient, 194 | PodReadyCounter: test.podReadyCounter, 195 | } 196 | metric, err := gatherer.GatherPerPod(test.metricName, test.namespace, test.objectRef, test.metricSelector) 197 | if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { 198 | t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) 199 | return 200 | } 201 | if !cmp.Equal(test.expected, metric) { 202 | t.Errorf("metrics mismatch (-want +got):\n%s", cmp.Diff(test.expected, metric)) 203 | } 204 | }) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /internal/external/gather_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package external_test 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | "time" 23 | 24 | "github.com/google/go-cmp/cmp" 25 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/external" 26 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/fake" 27 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/podutil" 28 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/testutil" 29 | externalmetrics "github.com/jthomperoo/k8shorizmetrics/v4/metrics/external" 30 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/value" 31 | metricsclient "github.com/jthomperoo/k8shorizmetrics/v4/metricsclient" 32 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 | "k8s.io/apimachinery/pkg/labels" 34 | ) 35 | 36 | func TestGather(t *testing.T) { 37 | equateErrorMessage := cmp.Comparer(func(x, y error) bool { 38 | if x == nil || y == nil { 39 | return x == nil && y == nil 40 | } 41 | return x.Error() == y.Error() 42 | }) 43 | 44 | var tests = []struct { 45 | description string 46 | expected *externalmetrics.Metric 47 | expectedErr error 48 | metricsclient metricsclient.Client 49 | podReadyCounter podutil.PodReadyCounter 50 | metricName string 51 | namespace string 52 | metricSelector *metav1.LabelSelector 53 | podSelector labels.Selector 54 | }{ 55 | { 56 | "Fail convert metric selector", 57 | nil, 58 | errors.New(`"invalid" is not a valid label selector operator`), 59 | nil, 60 | nil, 61 | "test-metric", 62 | "test-namespace", 63 | &metav1.LabelSelector{ 64 | MatchExpressions: []metav1.LabelSelectorRequirement{ 65 | { 66 | Operator: "invalid", 67 | }, 68 | }, 69 | }, 70 | nil, 71 | }, 72 | { 73 | "Fail to get metric", 74 | nil, 75 | errors.New("unable to get external metric test-namespace/test-metric/nil: fail to get metric"), 76 | &fake.MetricsClient{ 77 | GetExternalMetricReactor: func(metricName, namespace string, selector labels.Selector) ([]int64, time.Time, error) { 78 | return []int64{}, time.Time{}, errors.New("fail to get metric") 79 | }, 80 | }, 81 | nil, 82 | "test-metric", 83 | "test-namespace", 84 | nil, 85 | nil, 86 | }, 87 | { 88 | "Fail to get ready pods", 89 | nil, 90 | errors.New("unable to calculate ready pods: fail to get ready pods"), 91 | &fake.MetricsClient{ 92 | GetExternalMetricReactor: func(metricName, namespace string, selector labels.Selector) ([]int64, time.Time, error) { 93 | return []int64{}, time.Time{}, nil 94 | }, 95 | }, 96 | &fake.PodReadyCounter{ 97 | GetReadyPodsCountReactor: func(namespace string, selector labels.Selector) (int64, error) { 98 | return 0, errors.New("fail to get ready pods") 99 | }, 100 | }, 101 | "test-metric", 102 | "test-namespace", 103 | nil, 104 | nil, 105 | }, 106 | { 107 | "5 ready pods, 5 metrics, success", 108 | &externalmetrics.Metric{ 109 | ReadyPodCount: testutil.Int64Ptr(5), 110 | Current: value.MetricValue{ 111 | Value: testutil.Int64Ptr(15), 112 | }, 113 | }, 114 | nil, 115 | &fake.MetricsClient{ 116 | GetExternalMetricReactor: func(metricName, namespace string, selector labels.Selector) ([]int64, time.Time, error) { 117 | return []int64{1, 2, 3, 4, 5}, time.Time{}, nil 118 | }, 119 | }, 120 | &fake.PodReadyCounter{ 121 | GetReadyPodsCountReactor: func(namespace string, selector labels.Selector) (int64, error) { 122 | return 5, nil 123 | }, 124 | }, 125 | "test-metric", 126 | "test-namespace", 127 | nil, 128 | nil, 129 | }, 130 | } 131 | for _, test := range tests { 132 | t.Run(test.description, func(t *testing.T) { 133 | gatherer := &external.Gather{ 134 | MetricsClient: test.metricsclient, 135 | PodReadyCounter: test.podReadyCounter, 136 | } 137 | metric, err := gatherer.Gather(test.metricName, test.namespace, test.metricSelector, test.podSelector) 138 | if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { 139 | t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) 140 | return 141 | } 142 | if !cmp.Equal(test.expected, metric) { 143 | t.Errorf("metrics mismatch (-want +got):\n%s", cmp.Diff(test.expected, metric)) 144 | } 145 | }) 146 | } 147 | } 148 | 149 | func TestGatherPerPod(t *testing.T) { 150 | equateErrorMessage := cmp.Comparer(func(x, y error) bool { 151 | if x == nil || y == nil { 152 | return x == nil && y == nil 153 | } 154 | return x.Error() == y.Error() 155 | }) 156 | 157 | var tests = []struct { 158 | description string 159 | expected *externalmetrics.Metric 160 | expectedErr error 161 | metricsclient metricsclient.Client 162 | podReadyCounter podutil.PodReadyCounter 163 | metricName string 164 | namespace string 165 | metricSelector *metav1.LabelSelector 166 | }{ 167 | { 168 | "Fail convert metric selector", 169 | nil, 170 | errors.New(`"invalid" is not a valid label selector operator`), 171 | nil, 172 | nil, 173 | "test-metric", 174 | "test-namespace", 175 | &metav1.LabelSelector{ 176 | MatchExpressions: []metav1.LabelSelectorRequirement{ 177 | { 178 | Operator: "invalid", 179 | }, 180 | }, 181 | }, 182 | }, 183 | { 184 | "Fail to get metric", 185 | nil, 186 | errors.New("unable to get external metric test-namespace/test-metric/nil: fail to get metric"), 187 | &fake.MetricsClient{ 188 | GetExternalMetricReactor: func(metricName, namespace string, selector labels.Selector) ([]int64, time.Time, error) { 189 | return []int64{}, time.Time{}, errors.New("fail to get metric") 190 | }, 191 | }, 192 | nil, 193 | "test-metric", 194 | "test-namespace", 195 | nil, 196 | }, 197 | { 198 | "5 metrics, success", 199 | &externalmetrics.Metric{ 200 | Current: value.MetricValue{ 201 | AverageValue: testutil.Int64Ptr(15), 202 | }, 203 | }, 204 | nil, 205 | &fake.MetricsClient{ 206 | GetExternalMetricReactor: func(metricName, namespace string, selector labels.Selector) ([]int64, time.Time, error) { 207 | return []int64{1, 2, 3, 4, 5}, time.Time{}, nil 208 | }, 209 | }, 210 | nil, 211 | "test-metric", 212 | "test-namespace", 213 | nil, 214 | }, 215 | } 216 | for _, test := range tests { 217 | t.Run(test.description, func(t *testing.T) { 218 | gatherer := &external.Gather{ 219 | MetricsClient: test.metricsclient, 220 | PodReadyCounter: test.podReadyCounter, 221 | } 222 | metric, err := gatherer.GatherPerPod(test.metricName, test.namespace, test.metricSelector) 223 | if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { 224 | t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) 225 | return 226 | } 227 | if !cmp.Equal(test.expected, metric) { 228 | t.Errorf("metrics mismatch (-want +got):\n%s", cmp.Diff(test.expected, metric)) 229 | } 230 | }) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing to k8shorizmetrics 3 | 4 | First off, thanks for taking the time to contribute! ❤️ 5 | 6 | All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways 7 | to help and details about how this project handles them. Please make sure to read the relevant section before making 8 | your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. 9 | The community looks forward to your contributions. 🎉 10 | 11 | > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support 12 | > the project and show your appreciation, which we would also be very happy about: 13 | > - Star the project 14 | > - Tweet about it 15 | > - Refer this project in your project's readme 16 | > - Mention the project at local meetups and tell your friends/colleagues 17 | 18 | 19 | ## Table of Contents 20 | 21 | - [Code of Conduct](#code-of-conduct) 22 | - [I Have a Question](#i-have-a-question) 23 | - [I Want To Contribute](#i-want-to-contribute) 24 | - [Reporting Bugs](#reporting-bugs) 25 | - [Suggesting Enhancements](#suggesting-enhancements) 26 | 27 | ## Code of Conduct 28 | 29 | This project and everyone participating in it is governed by the 30 | [k8shorizmetrics Code of Conduct](https://github.com/jthomperoo/k8shorizmetrics/blob/master/CODE_OF_CONDUCT.md). 31 | By participating, you are expected to uphold this code. Please report unacceptable behavior 32 | to j.thomperoo@hotmail.com. 33 | 34 | ## I Have a Question 35 | 36 | > If you want to ask a question, we assume that you have read the available 37 | > [Documentation](https://k8shorizmetrics.readthedocs.io/en/latest/). 38 | 39 | Before you ask a question, it is best to search for existing 40 | [Issues](https://github.com/jthomperoo/k8shorizmetrics/issues) that might help you. In case you have found a 41 | suitable issue and still need clarification, you can write your question in this issue. It is also advisable to 42 | search the internet for answers first. 43 | 44 | If you then still feel the need to ask a question and need clarification, we recommend the following: 45 | 46 | - Open an [Issue](https://github.com/jthomperoo/k8shorizmetrics/issues/new). 47 | - Provide as much context as you can about what you're running into. 48 | - Provide project and platform versions, depending on what seems relevant. 49 | 50 | We will then take care of the issue as soon as possible. 51 | 52 | ## I Want To Contribute 53 | 54 | > ### Legal Notice 55 | > When contributing to this project, you must agree that you have authored 100% of the content, that you have the 56 | > necessary rights to the content and that the content you contribute may be provided under the project license. 57 | 58 | ### Reporting Bugs 59 | 60 | 61 | #### Before Submitting a Bug Report 62 | 63 | A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to 64 | investigate carefully, collect information and describe the issue in detail in your report. Please complete the 65 | following steps in advance to help us fix any potential bug as fast as possible. 66 | 67 | - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment 68 | components/versions (Make sure that you have read the 69 | [documentation](https://k8shorizmetrics.readthedocs.io/en/latest/). If you are looking for support, you might 70 | want to check [this section](#i-have-a-question)). 71 | - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there 72 | is not already a bug report existing for your bug or error in the 73 | [bug tracker](https://github.com/jthomperoo/k8shorizmetrics/issues). 74 | - Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have 75 | discussed the issue. 76 | - Collect information about the bug: 77 | - Kubernetes version. 78 | - Any libraries/tooling that you are using that may affect it. 79 | - Can you reliably reproduce the issue? And can you also reproduce it with older versions? 80 | 81 | 82 | #### How Do I Submit a Good Bug Report? 83 | 84 | We use GitHub issues to track bugs and errors. If you run into an issue with the project: 85 | 86 | - Open an [Issue](https://github.com/jthomperoo/k8shorizmetrics/issues/new). (Since we can't be sure at this point 87 | whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) 88 | - Explain the behavior you would expect and the actual behavior. 89 | - Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to 90 | recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem 91 | and create a reduced test case. 92 | - Provide the information you collected in the previous section. 93 | 94 | Once it's filed: 95 | 96 | - Someone will try to reproduce the issue with your provided steps. 97 | - If someone is able to reproduce the issue the issue will be left to be [implemented by 98 | someone](#your-first-code-contribution). 99 | 100 | ### Suggesting Enhancements 101 | 102 | This section guides you through submitting an enhancement suggestion for k8shorizmetrics, 103 | **including completely new features and minor improvements to existing functionality**. Following these guidelines will 104 | help maintainers and the community to understand your suggestion and find related suggestions. 105 | 106 | 107 | #### Before Submitting an Enhancement 108 | 109 | - Make sure that you are using the latest version. 110 | - Read the [documentation](https://k8shorizmetrics.readthedocs.io/en/latest/) carefully and find out if the 111 | functionality is already covered, maybe by an individual configuration. 112 | - Perform a [search](https://github.com/jthomperoo/k8shorizmetrics/issues) to see if the enhancement has already 113 | been suggested. If it has, add a comment to the existing issue instead of opening a new one. 114 | - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to 115 | convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful 116 | to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider 117 | writing an add-on/plugin library. 118 | 119 | 120 | #### How Do I Submit a Good Enhancement Suggestion? 121 | 122 | Enhancement suggestions are tracked as [GitHub issues](https://github.com/jthomperoo/k8shorizmetrics/issues). 123 | 124 | - Use a **clear and descriptive title** for the issue to identify the suggestion. 125 | - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. 126 | - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point 127 | you can also tell which alternatives do not work for you. 128 | - **Explain why this enhancement would be useful** to most k8shorizmetrics users. You may also want to point out 129 | the other projects that solved it better and which could serve as inspiration. 130 | 131 | ## Developing 132 | 133 | ### Environment 134 | 135 | Developing this project requires these dependencies: 136 | 137 | * [Go v1.22+](https://go.dev/doc/install) 138 | 139 | It is recommended to test locally using a local Kubernetes managment system, such as 140 | [k3d](https://github.com/rancher/k3d) (allows running a small Kubernetes cluster locally using Docker). 141 | 142 | ### Commands 143 | 144 | * `make test` - runs the unit tests. 145 | * `make lint` - lints the code. 146 | * `make format` - formats the code, must be run to pass the CI. 147 | * `make view_coverage` - opens up any generated coverage reports in the browser. 148 | 149 | ## Styleguides 150 | 151 | ### Commit messages 152 | 153 | Commit messages should follow the ['How to Write a Git Commit Message'](https://chris.beams.io/posts/git-commit/) guide. 154 | 155 | ### Documentation 156 | 157 | Documentation should be in plain english, with 120 character max line width. 158 | 159 | ### Code 160 | 161 | Project code should pass the linter and all tests should pass. 162 | 163 | ## Attribution 164 | 165 | This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)! 166 | -------------------------------------------------------------------------------- /metricsclient/metricsclient.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package metricsclient provides a standard and easily instantiated way of querying metrics from the K8s APIs. 18 | package metricsclient 19 | 20 | import ( 21 | "context" 22 | "fmt" 23 | "time" 24 | 25 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/podmetrics" 26 | autoscalingv2 "k8s.io/api/autoscaling/v2" 27 | v1 "k8s.io/api/core/v1" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "k8s.io/apimachinery/pkg/labels" 30 | "k8s.io/apimachinery/pkg/runtime/schema" 31 | "k8s.io/client-go/discovery" 32 | cacheddiscovery "k8s.io/client-go/discovery/cached" 33 | "k8s.io/client-go/rest" 34 | "k8s.io/client-go/restmapper" 35 | custommetricsv1 "k8s.io/metrics/pkg/apis/custom_metrics/v1beta2" 36 | metricsv1beta1 "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1" 37 | "k8s.io/metrics/pkg/client/custom_metrics" 38 | "k8s.io/metrics/pkg/client/external_metrics" 39 | ) 40 | 41 | const ( 42 | metricServerDefaultMetricWindow = time.Minute 43 | ) 44 | 45 | // Client allows for retrieval of Kubernetes metrics 46 | type Client interface { 47 | GetResourceMetric(resource v1.ResourceName, namespace string, selector labels.Selector) (podmetrics.MetricsInfo, time.Time, error) 48 | GetRawMetric(metricName string, namespace string, selector labels.Selector, metricSelector labels.Selector) (podmetrics.MetricsInfo, time.Time, error) 49 | GetObjectMetric(metricName string, namespace string, objectRef *autoscalingv2.CrossVersionObjectReference, metricSelector labels.Selector) (int64, time.Time, error) 50 | GetExternalMetric(metricName, namespace string, selector labels.Selector) ([]int64, time.Time, error) 51 | } 52 | 53 | func NewClient(clusterConfig *rest.Config, discovery discovery.DiscoveryInterface) *RESTClient { 54 | return &RESTClient{ 55 | Client: metricsv1beta1.NewForConfigOrDie(clusterConfig), 56 | ExternalMetricsClient: external_metrics.NewForConfigOrDie(clusterConfig), 57 | CustomMetricsClient: custom_metrics.NewForConfig( 58 | clusterConfig, 59 | restmapper.NewDeferredDiscoveryRESTMapper(cacheddiscovery.NewMemCacheClient(discovery)), 60 | custom_metrics.NewAvailableAPIsGetter(discovery), 61 | ), 62 | } 63 | } 64 | 65 | // RESTClient retrieves Kubernetes metrics through the Kubernetes REST API 66 | type RESTClient struct { 67 | Client metricsv1beta1.MetricsV1beta1Interface 68 | ExternalMetricsClient external_metrics.ExternalMetricsClient 69 | CustomMetricsClient custom_metrics.CustomMetricsClient 70 | } 71 | 72 | // GetResourceMetric gets the given resource metric (and an associated oldest timestamp) 73 | // for all pods matching the specified selector in the given namespace 74 | func (c *RESTClient) GetResourceMetric(resource v1.ResourceName, namespace string, selector labels.Selector) (podmetrics.MetricsInfo, time.Time, error) { 75 | metrics, err := c.Client.PodMetricses(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: selector.String()}) 76 | if err != nil { 77 | return nil, time.Time{}, fmt.Errorf("unable to fetch metrics from resource metrics API: %v", err) 78 | } 79 | 80 | if len(metrics.Items) == 0 { 81 | return nil, time.Time{}, fmt.Errorf("no metrics returned from resource metrics API") 82 | } 83 | 84 | res := make(podmetrics.MetricsInfo, len(metrics.Items)) 85 | for _, m := range metrics.Items { 86 | podSum := int64(0) 87 | missing := len(m.Containers) == 0 88 | for _, c := range m.Containers { 89 | resValue, found := c.Usage[resource] 90 | if !found { 91 | missing = true 92 | break 93 | } 94 | podSum += resValue.MilliValue() 95 | } 96 | if !missing { 97 | res[m.Name] = podmetrics.Metric{ 98 | Timestamp: m.Timestamp.Time, 99 | Window: m.Window.Duration, 100 | Value: podSum, 101 | } 102 | } 103 | } 104 | 105 | timestamp := metrics.Items[0].Timestamp.Time 106 | 107 | return res, timestamp, nil 108 | } 109 | 110 | // GetRawMetric gets the given metric (and an associated oldest timestamp) 111 | // for all pods matching the specified selector in the given namespace 112 | func (c *RESTClient) GetRawMetric(metricName string, namespace string, selector labels.Selector, metricSelector labels.Selector) (podmetrics.MetricsInfo, time.Time, error) { 113 | metrics, err := c.CustomMetricsClient.NamespacedMetrics(namespace).GetForObjects(schema.GroupKind{Kind: "Pod"}, selector, metricName, metricSelector) 114 | if err != nil { 115 | return nil, time.Time{}, fmt.Errorf("unable to fetch metrics from custom metrics API: %v", err) 116 | } 117 | 118 | if len(metrics.Items) == 0 { 119 | return nil, time.Time{}, fmt.Errorf("no metrics returned from custom metrics API") 120 | } 121 | 122 | res := make(podmetrics.MetricsInfo, len(metrics.Items)) 123 | for _, m := range metrics.Items { 124 | window := metricServerDefaultMetricWindow 125 | if m.WindowSeconds != nil { 126 | window = time.Duration(*m.WindowSeconds) * time.Second 127 | } 128 | res[m.DescribedObject.Name] = podmetrics.Metric{ 129 | Timestamp: m.Timestamp.Time, 130 | Window: window, 131 | Value: int64(m.Value.MilliValue()), 132 | } 133 | 134 | m.Value.MilliValue() 135 | } 136 | 137 | timestamp := metrics.Items[0].Timestamp.Time 138 | 139 | return res, timestamp, nil 140 | } 141 | 142 | // GetObjectMetric gets the given metric (and an associated timestamp) for the given 143 | // object in the given namespace 144 | func (c *RESTClient) GetObjectMetric(metricName string, namespace string, objectRef *autoscalingv2.CrossVersionObjectReference, metricSelector labels.Selector) (int64, time.Time, error) { 145 | gvk := schema.FromAPIVersionAndKind(objectRef.APIVersion, objectRef.Kind) 146 | var metricValue *custommetricsv1.MetricValue 147 | var err error 148 | if gvk.Kind == "Namespace" && gvk.Group == "" { 149 | // handle namespace separately 150 | // NB: we ignore namespace name here, since CrossVersionObjectReference isn't 151 | // supposed to allow you to escape your namespace 152 | metricValue, err = c.CustomMetricsClient.RootScopedMetrics().GetForObject(gvk.GroupKind(), namespace, metricName, metricSelector) 153 | } else { 154 | metricValue, err = c.CustomMetricsClient.NamespacedMetrics(namespace).GetForObject(gvk.GroupKind(), objectRef.Name, metricName, metricSelector) 155 | } 156 | 157 | if err != nil { 158 | return 0, time.Time{}, fmt.Errorf("unable to fetch metrics from custom metrics API: %v", err) 159 | } 160 | 161 | return metricValue.Value.MilliValue(), metricValue.Timestamp.Time, nil 162 | } 163 | 164 | // GetExternalMetric gets all the values of a given external metric 165 | // that match the specified selector. 166 | func (c *RESTClient) GetExternalMetric(metricName, namespace string, selector labels.Selector) ([]int64, time.Time, error) { 167 | metrics, err := c.ExternalMetricsClient.NamespacedMetrics(namespace).List(metricName, selector) 168 | if err != nil { 169 | return []int64{}, time.Time{}, fmt.Errorf("unable to fetch metrics from external metrics API: %v", err) 170 | } 171 | 172 | if len(metrics.Items) == 0 { 173 | return nil, time.Time{}, fmt.Errorf("no metrics returned from external metrics API") 174 | } 175 | 176 | res := make([]int64, 0) 177 | for _, m := range metrics.Items { 178 | res = append(res, m.Value.MilliValue()) 179 | } 180 | timestamp := metrics.Items[0].Timestamp.Time 181 | return res, timestamp, nil 182 | } 183 | 184 | // GetResourceUtilizationRatio takes in a set of metrics, a set of matching requests, 185 | // and a target utilization percentage, and calculates the ratio of 186 | // desired to actual utilization (returning that, the actual utilization, and the raw average value) 187 | func GetResourceUtilizationRatio(metrics podmetrics.MetricsInfo, requests map[string]int64, targetUtilization int32) (utilizationRatio float64, currentUtilization int32, rawAverageValue int64, err error) { 188 | metricsTotal := int64(0) 189 | requestsTotal := int64(0) 190 | numEntries := 0 191 | 192 | for podName, metric := range metrics { 193 | request, hasRequest := requests[podName] 194 | if !hasRequest { 195 | // we check for missing requests elsewhere, so assuming missing requests == extraneous metrics 196 | continue 197 | } 198 | 199 | metricsTotal += metric.Value 200 | requestsTotal += request 201 | numEntries++ 202 | } 203 | 204 | // if the set of requests is completely disjoint from the set of metrics, 205 | // then we could have an issue where the requests total is zero 206 | if requestsTotal == 0 { 207 | return 0, 0, 0, fmt.Errorf("no metrics returned matched known pods") 208 | } 209 | 210 | currentUtilization = int32((metricsTotal * 100) / requestsTotal) 211 | 212 | return float64(currentUtilization) / float64(targetUtilization), currentUtilization, metricsTotal / int64(numEntries), nil 213 | } 214 | 215 | // GetMetricUtilizationRatio takes in a set of metrics and a target utilization value, 216 | // and calculates the ratio of desired to actual utilization 217 | // (returning that and the actual utilization) 218 | func GetMetricUtilizationRatio(metrics podmetrics.MetricsInfo, targetUtilization int64) (utilizationRatio float64, currentUtilization int64) { 219 | metricsTotal := int64(0) 220 | for _, metric := range metrics { 221 | metricsTotal += metric.Value 222 | } 223 | 224 | currentUtilization = metricsTotal / int64(len(metrics)) 225 | 226 | return float64(currentUtilization) / float64(targetUtilization), currentUtilization 227 | } 228 | -------------------------------------------------------------------------------- /internal/resource/evaluate_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The K8sHorizMetrics Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package resource_test 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | 23 | "github.com/google/go-cmp/cmp" 24 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/fake" 25 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/replicas" 26 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/resource" 27 | "github.com/jthomperoo/k8shorizmetrics/v4/internal/testutil" 28 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics" 29 | "github.com/jthomperoo/k8shorizmetrics/v4/metrics/podmetrics" 30 | resourcemetrics "github.com/jthomperoo/k8shorizmetrics/v4/metrics/resource" 31 | v2 "k8s.io/api/autoscaling/v2" 32 | k8sresource "k8s.io/apimachinery/pkg/api/resource" 33 | "k8s.io/apimachinery/pkg/util/sets" 34 | ) 35 | 36 | func TestEvaluate(t *testing.T) { 37 | equateErrorMessage := cmp.Comparer(func(x, y error) bool { 38 | if x == nil || y == nil { 39 | return x == nil && y == nil 40 | } 41 | return x.Error() == y.Error() 42 | }) 43 | 44 | var tests = []struct { 45 | description string 46 | expected int32 47 | expectedErr error 48 | calculater replicas.Calculator 49 | tolerance float64 50 | currentReplicas int32 51 | gatheredMetric *metrics.Metric 52 | }{ 53 | { 54 | "Invalid metric source", 55 | 0, 56 | errors.New("invalid resource metric source: neither a utilization target nor a value target was set"), 57 | nil, 58 | 0, 59 | 3, 60 | &metrics.Metric{ 61 | Spec: v2.MetricSpec{ 62 | Resource: &v2.ResourceMetricSource{}, 63 | }, 64 | }, 65 | }, 66 | { 67 | "Success, average value", 68 | 6, 69 | nil, 70 | &fake.Calculate{ 71 | GetPlainMetricReplicaCountReactor: func(metrics podmetrics.MetricsInfo, currentReplicas int32, targetUtilization, readyPodCount int64, missingPods, ignoredPods sets.String) int32 { 72 | return 6 73 | }, 74 | }, 75 | 0, 76 | 5, 77 | &metrics.Metric{ 78 | Spec: v2.MetricSpec{ 79 | Resource: &v2.ResourceMetricSource{ 80 | Target: v2.MetricTarget{ 81 | AverageValue: k8sresource.NewMilliQuantity(50, k8sresource.DecimalSI), 82 | }, 83 | }, 84 | }, 85 | Resource: &resourcemetrics.Metric{ 86 | PodMetricsInfo: podmetrics.MetricsInfo{}, 87 | ReadyPodCount: 3, 88 | IgnoredPods: sets.String{"ignored": {}}, 89 | MissingPods: sets.String{"missing": {}}, 90 | }, 91 | }, 92 | }, 93 | { 94 | "Fail, average utilization, no metrics for pods", 95 | 0, 96 | errors.New(`no metrics returned matched known pods`), 97 | nil, 98 | 0, 99 | 3, 100 | &metrics.Metric{ 101 | Spec: v2.MetricSpec{ 102 | Resource: &v2.ResourceMetricSource{ 103 | Target: v2.MetricTarget{ 104 | AverageUtilization: testutil.Int32Ptr(15), 105 | }, 106 | }, 107 | }, 108 | Resource: &resourcemetrics.Metric{ 109 | PodMetricsInfo: podmetrics.MetricsInfo{}, 110 | Requests: map[string]int64{}, 111 | ReadyPodCount: 3, 112 | IgnoredPods: sets.String{"ignored": {}}, 113 | MissingPods: sets.String{"missing": {}}, 114 | }, 115 | }, 116 | }, 117 | { 118 | "Success, average utilization, no ignored pods, no missing pods, within tolerance, no scale change", 119 | 2, 120 | nil, 121 | nil, 122 | 0, 123 | 2, 124 | &metrics.Metric{ 125 | Spec: v2.MetricSpec{ 126 | Resource: &v2.ResourceMetricSource{ 127 | Target: v2.MetricTarget{ 128 | AverageUtilization: testutil.Int32Ptr(50), 129 | }, 130 | }, 131 | }, 132 | Resource: &resourcemetrics.Metric{ 133 | PodMetricsInfo: podmetrics.MetricsInfo{ 134 | "pod-1": podmetrics.Metric{ 135 | Value: 5, 136 | }, 137 | "pod-2": podmetrics.Metric{ 138 | Value: 5, 139 | }, 140 | }, 141 | Requests: map[string]int64{ 142 | "pod-1": 10, 143 | "pod-2": 10, 144 | }, 145 | ReadyPodCount: 2, 146 | IgnoredPods: sets.String{}, 147 | MissingPods: sets.String{}, 148 | }, 149 | }, 150 | }, 151 | { 152 | "Success, average utilization, no ignored pods, no missing pods, beyond tolerance, scale up", 153 | 8, 154 | nil, 155 | nil, 156 | 0, 157 | 2, 158 | &metrics.Metric{ 159 | Spec: v2.MetricSpec{ 160 | Resource: &v2.ResourceMetricSource{ 161 | Target: v2.MetricTarget{ 162 | AverageUtilization: testutil.Int32Ptr(50), 163 | }, 164 | }, 165 | }, 166 | Resource: &resourcemetrics.Metric{ 167 | PodMetricsInfo: podmetrics.MetricsInfo{ 168 | "pod-1": podmetrics.Metric{ 169 | Value: 20, 170 | }, 171 | "pod-2": podmetrics.Metric{ 172 | Value: 20, 173 | }, 174 | }, 175 | Requests: map[string]int64{ 176 | "pod-1": 10, 177 | "pod-2": 10, 178 | }, 179 | ReadyPodCount: 2, 180 | IgnoredPods: sets.String{}, 181 | MissingPods: sets.String{}, 182 | }, 183 | }, 184 | }, 185 | { 186 | "Success, average utilization, no ignored pods, no missing pods, beyond tolerance, scale down", 187 | 1, 188 | nil, 189 | nil, 190 | 0, 191 | 2, 192 | &metrics.Metric{ 193 | Spec: v2.MetricSpec{ 194 | Resource: &v2.ResourceMetricSource{ 195 | Target: v2.MetricTarget{ 196 | AverageUtilization: testutil.Int32Ptr(50), 197 | }, 198 | }, 199 | }, 200 | Resource: &resourcemetrics.Metric{ 201 | PodMetricsInfo: podmetrics.MetricsInfo{ 202 | "pod-1": podmetrics.Metric{ 203 | Value: 2, 204 | }, 205 | "pod-2": podmetrics.Metric{ 206 | Value: 2, 207 | }, 208 | }, 209 | Requests: map[string]int64{ 210 | "pod-1": 10, 211 | "pod-2": 10, 212 | }, 213 | ReadyPodCount: 2, 214 | IgnoredPods: sets.String{}, 215 | MissingPods: sets.String{}, 216 | }, 217 | }, 218 | }, 219 | { 220 | "Success, average utilization, no ignored pods, 2 missing pods, beyond tolerance, scale up", 221 | 8, 222 | nil, 223 | nil, 224 | 0, 225 | 4, 226 | &metrics.Metric{ 227 | Spec: v2.MetricSpec{ 228 | Resource: &v2.ResourceMetricSource{ 229 | Target: v2.MetricTarget{ 230 | AverageUtilization: testutil.Int32Ptr(50), 231 | }, 232 | }, 233 | }, 234 | Resource: &resourcemetrics.Metric{ 235 | PodMetricsInfo: podmetrics.MetricsInfo{ 236 | "pod-1": podmetrics.Metric{ 237 | Value: 20, 238 | }, 239 | "pod-2": podmetrics.Metric{ 240 | Value: 20, 241 | }, 242 | }, 243 | Requests: map[string]int64{ 244 | "pod-1": 10, 245 | "pod-2": 10, 246 | "missing-1": 10, 247 | "missing-2": 10, 248 | }, 249 | ReadyPodCount: 2, 250 | IgnoredPods: sets.String{}, 251 | MissingPods: sets.String{ 252 | "missing-1": {}, 253 | "missing-2": {}, 254 | }, 255 | }, 256 | }, 257 | }, 258 | { 259 | "Success, average utilization, no ignored pods, 2 missing pods, beyond tolerance, scale down", 260 | 2, 261 | nil, 262 | nil, 263 | 0, 264 | 4, 265 | &metrics.Metric{ 266 | Spec: v2.MetricSpec{ 267 | Resource: &v2.ResourceMetricSource{ 268 | Target: v2.MetricTarget{ 269 | AverageUtilization: testutil.Int32Ptr(50), 270 | }, 271 | }, 272 | }, 273 | Resource: &resourcemetrics.Metric{ 274 | PodMetricsInfo: podmetrics.MetricsInfo{ 275 | "pod-1": podmetrics.Metric{ 276 | Value: 1, 277 | }, 278 | "pod-2": podmetrics.Metric{ 279 | Value: 1, 280 | }, 281 | }, 282 | Requests: map[string]int64{ 283 | "pod-1": 20, 284 | "pod-2": 20, 285 | "missing-1": 3, 286 | "missing-2": 3, 287 | }, 288 | ReadyPodCount: 2, 289 | IgnoredPods: sets.String{}, 290 | MissingPods: sets.String{ 291 | "missing-1": {}, 292 | "missing-2": {}, 293 | }, 294 | }, 295 | }, 296 | }, 297 | { 298 | "Success, average utilization, 2 ignored pods, 2 missing pods, beyond tolerance, scale up", 299 | 12, 300 | nil, 301 | nil, 302 | 0, 303 | 4, 304 | &metrics.Metric{ 305 | Spec: v2.MetricSpec{ 306 | Resource: &v2.ResourceMetricSource{ 307 | Target: v2.MetricTarget{ 308 | AverageUtilization: testutil.Int32Ptr(50), 309 | }, 310 | }, 311 | }, 312 | Resource: &resourcemetrics.Metric{ 313 | PodMetricsInfo: podmetrics.MetricsInfo{ 314 | "pod-1": podmetrics.Metric{ 315 | Value: 20, 316 | }, 317 | "pod-2": podmetrics.Metric{ 318 | Value: 20, 319 | }, 320 | }, 321 | Requests: map[string]int64{ 322 | "pod-1": 10, 323 | "pod-2": 10, 324 | "missing-1": 5, 325 | "missing-2": 5, 326 | "ignored-1": 5, 327 | "ignored-2": 5, 328 | }, 329 | ReadyPodCount: 2, 330 | IgnoredPods: sets.String{ 331 | "ignored-1": {}, 332 | "ignored-2": {}, 333 | }, 334 | MissingPods: sets.String{ 335 | "missing-1": {}, 336 | "missing-2": {}, 337 | }, 338 | }, 339 | }, 340 | }, 341 | { 342 | "Success, average utilization, 2 ignored pods, 2 missing pods, within tolerance, no scale change", 343 | 4, 344 | nil, 345 | nil, 346 | 0.5, 347 | 4, 348 | &metrics.Metric{ 349 | Spec: v2.MetricSpec{ 350 | Resource: &v2.ResourceMetricSource{ 351 | Target: v2.MetricTarget{ 352 | AverageUtilization: testutil.Int32Ptr(50), 353 | }, 354 | }, 355 | }, 356 | Resource: &resourcemetrics.Metric{ 357 | PodMetricsInfo: podmetrics.MetricsInfo{ 358 | "pod-1": podmetrics.Metric{ 359 | Value: 20, 360 | }, 361 | "pod-2": podmetrics.Metric{ 362 | Value: 20, 363 | }, 364 | }, 365 | Requests: map[string]int64{ 366 | "pod-1": 10, 367 | "pod-2": 10, 368 | "missing-1": 10, 369 | "missing-2": 10, 370 | "ignored-1": 10, 371 | "ignored-2": 10, 372 | }, 373 | ReadyPodCount: 2, 374 | IgnoredPods: sets.String{ 375 | "ignored-1": {}, 376 | "ignored-2": {}, 377 | }, 378 | MissingPods: sets.String{ 379 | "missing-1": {}, 380 | "missing-2": {}, 381 | }, 382 | }, 383 | }, 384 | }, 385 | } 386 | for _, test := range tests { 387 | t.Run(test.description, func(t *testing.T) { 388 | evaluater := resource.Evaluate{ 389 | Calculater: test.calculater, 390 | } 391 | evaluation, err := evaluater.Evaluate(test.currentReplicas, test.gatheredMetric, test.tolerance) 392 | if !cmp.Equal(&err, &test.expectedErr, equateErrorMessage) { 393 | t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) 394 | return 395 | } 396 | if !cmp.Equal(test.expected, evaluation) { 397 | t.Errorf("evaluation mismatch (-want +got):\n%s", cmp.Diff(test.expected, evaluation)) 398 | } 399 | }) 400 | } 401 | } 402 | --------------------------------------------------------------------------------