├── .dockerignore ├── .github └── workflows │ ├── go-lint-test-build.yaml │ ├── helm-lint-test.yaml │ └── helm-release.yaml ├── .gitignore ├── .goreleaser.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── charts └── pvc-autoscaler │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── _helpers.tpl │ ├── clusterrole.yaml │ ├── clusterrolebinding.yaml │ ├── deployment.yaml │ └── serviceaccount.yaml │ └── values.yaml ├── cmd ├── kubeclient.go ├── main.go ├── metrics.go ├── reconcile.go └── utils.go ├── docs └── pvc-autoscaler-architecture.svg ├── go.mod ├── go.sum ├── internal └── metrics_clients │ ├── clients │ └── clients.go │ └── prometheus │ ├── mock_prometheus_api.go │ ├── prometheus.go │ └── prometheus_test.go └── renovate.json /.dockerignore: -------------------------------------------------------------------------------- 1 | bin 2 | dist 3 | charts 4 | local_tests 5 | 6 | .git 7 | .goreleaser.yaml 8 | coverage.* 9 | -------------------------------------------------------------------------------- /.github/workflows/go-lint-test-build.yaml: -------------------------------------------------------------------------------- 1 | name: Go format, vet, test and build 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | 11 | - name: Set up Go 12 | uses: actions/setup-go@v5 13 | with: 14 | go-version: '1.20' 15 | check-latest: true 16 | cache-dependency-path: go.sum 17 | 18 | - name: Run fmt 19 | run: go fmt ./... 20 | 21 | - name: Run vet 22 | run: go vet ./... 23 | 24 | - name: Run test 25 | run: go test -v ./... 26 | 27 | - name: Run build 28 | run: go build -o bin/pvc-autoscaler ./cmd 29 | -------------------------------------------------------------------------------- /.github/workflows/helm-lint-test.yaml: -------------------------------------------------------------------------------- 1 | name: Lint and Test Helm Charts 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | lint-test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | 14 | - name: Set up Helm 15 | uses: azure/setup-helm@v4 16 | with: 17 | version: v3.12.1 18 | 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version: '3.9' 22 | check-latest: true 23 | 24 | - name: Set up chart-testing 25 | uses: helm/chart-testing-action@v2.6.1 26 | 27 | - name: Run chart-testing (list-changed) 28 | id: list-changed 29 | run: | 30 | changed=$(ct list-changed --target-branch main) 31 | if [[ -n "$changed" ]]; then 32 | echo "changed=true" >> "$GITHUB_OUTPUT" 33 | fi 34 | 35 | - name: Run chart-testing (lint) 36 | if: steps.list-changed.outputs.changed == 'true' 37 | run: ct lint --target-branch main 38 | -------------------------------------------------------------------------------- /.github/workflows/helm-release.yaml: -------------------------------------------------------------------------------- 1 | name: Release Charts 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | permissions: 11 | contents: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Configure Git 20 | run: | 21 | git config user.name "$GITHUB_ACTOR" 22 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 23 | 24 | - name: Install Helm 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | uses: azure/setup-helm@v4 28 | 29 | - name: Run chart-releaser 30 | uses: helm/chart-releaser-action@v1.6.0 31 | env: 32 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | /dist 24 | /bin 25 | /local_test 26 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | # you may remove this if you don't need go generate 8 | - go generate ./... 9 | builds: 10 | - main: ./cmd 11 | env: 12 | - CGO_ENABLED=0 13 | goos: 14 | - linux 15 | - windows 16 | - darwin 17 | 18 | archives: 19 | - format: tar.gz 20 | # this name template makes the OS and Arch compatible with the results of uname. 21 | name_template: >- 22 | {{ .ProjectName }}_ 23 | {{- title .Os }}_ 24 | {{- if eq .Arch "amd64" }}x86_64 25 | {{- else if eq .Arch "386" }}i386 26 | {{- else }}{{ .Arch }}{{ end }} 27 | {{- if .Arm }}v{{ .Arm }}{{ end }} 28 | # use zip for windows archives 29 | format_overrides: 30 | - goos: windows 31 | format: zip 32 | checksum: 33 | name_template: 'checksums.txt' 34 | snapshot: 35 | name_template: "{{ incpatch .Version }}-next" 36 | changelog: 37 | sort: asc 38 | filters: 39 | exclude: 40 | - '^docs:' 41 | - '^test:' 42 | 43 | # The lines beneath this are called `modelines`. See `:help modeline` 44 | # Feel free to remove those if you don't want/use them. 45 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 46 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22 as build 2 | 3 | WORKDIR /go/pvc-autoscaler 4 | 5 | COPY go.mod go.sum /go/pvc-autoscaler/ 6 | RUN go mod download && go mod verify 7 | 8 | COPY cmd /go/pvc-autoscaler/cmd 9 | COPY internal /go/pvc-autoscaler/internal 10 | 11 | RUN --mount=type=cache,id=gomod,target=/go/pkg/mod \ 12 | --mount=type=cache,id=gobuild,target=/root/.cache/go-build \ 13 | GOOS=linux CGO_ENABLED=0 go build -v -o /go/bin/pvc-autoscaler /go/pvc-autoscaler/cmd 14 | 15 | 16 | FROM gcr.io/distroless/static-debian11 17 | COPY --from=build /go/bin/pvc-autoscaler / 18 | ENTRYPOINT ["/pvc-autoscaler"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lorenzo Maffioli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /usr/bin/env bash -o pipefail 2 | .SHELLFLAGS = -ec 3 | 4 | PLATFORM ?= linux/amd64 5 | IMG ?= lorenzophys/pvc-autoscaler:dev 6 | 7 | .PHONY: help 8 | help: ## Display this help. 9 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 10 | 11 | .PHONY: fmt 12 | fmt: ## Run go fmt against code. 13 | @go fmt $$(go list ./... | grep -v 'mock_*') 14 | 15 | .PHONY: vet 16 | vet: fmt ## Run go vet against code. 17 | @go vet $$(go list ./... | grep -v 'mock_*') 18 | 19 | .PHONY: test 20 | test: fmt vet ## Run go test against code. 21 | @go test $$(go list ./... | grep -v 'mock_*') -v 22 | 23 | .PHONY: cov 24 | cov: fmt vet ## Run go test with coverage against code. 25 | go test $$(go list ./... | grep -v 'mock_*') -coverprofile=coverage.out 26 | 27 | .PHONY: cov-html 28 | cov-html: ## Display the coverage.out in html form. 29 | go tool cover -html=coverage.out 30 | 31 | .PHONY: build 32 | build: fmt vet test ## Build the autoscaler binary. 33 | @go build -o bin/pvc-autoscaler ./cmd 34 | 35 | .PHONY: run 36 | run: fmt vet ## Run the autoscaler locally. 37 | @go run ./main.go 38 | 39 | .PHONY: docker-build 40 | docker-build: fmt vet ## Build the docker image with the autoscaler. 41 | @docker build -t ${IMG} --platform ${PLATFORM} . 42 | 43 | .PHONY: docker-push 44 | docker-push: ## Push the docker image with the autoscaler. 45 | @docker push ${IMG} 46 | 47 | .PHONY: docker-run 48 | docker-run: ## Run the docker image with the autoscaler. 49 | @docker run --rm ${IMG} 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/lorenzophys/pvc-autoscaler)](https://goreportcard.com/report/github.com/lorenzophys/pvc-autoscaler) 2 | ![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/lorenzophys/pvc-autoscaler/go-lint-test-build.yaml?logo=Go) 3 | ![GitHub release (with filter)](https://img.shields.io/github/v/release/lorenzophys/pvc-autoscaler?filter=v*&logo=Go) 4 | ![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/lorenzophys/pvc-autoscaler/helm-lint-test.yaml?logo=helm&label=Helm) 5 | ![GitHub release (with filter)](https://img.shields.io/github/v/release/lorenzophys/pvc-autoscaler?filter=pvcautoscaler-*&logo=Helm&label=Helm%20release) 6 | ![GitHub](https://img.shields.io/github/license/lorenzophys/pvc-autoscaler) 7 | 8 | # PVC autoscaler for Kubernetes 9 | 10 | PVC Autoscaler is an open-source project aimed at providing autoscaling functionality to Persistent Volume Claims (PVCs) in Kubernetes environments. It allows you to automatically scale your PVCs based on your workloads and the metrics collected. 11 | 12 | Please note that PVC Autoscaler is currently in a heavy development phase. As such, it's not recommended for production usage at this point. 13 | 14 | ## Motivation 15 | 16 | The motivation behind the PVC Autoscaler project is to provide developers with an easy and efficient way of managing storage resources within their Kubernetes clusters: sometimes is difficult to estimate how much storage an application needs. With the PVC Autoscaler, there's no need to manually adjust the size of your PVCs as your storage needs change. The Autoscaler handles this for you, freeing you up to focus on other areas of your development work. 17 | 18 | ## How it works 19 | 20 | ![pvc-autoscaler-architecture](https://github.com/lorenzophys/pvc-autoscaler/assets/63981558/5dce9455-c7e1-49df-ba1c-4f88964139a3) 21 | 22 | ## Limitations 23 | 24 | Currently it only supports Prometheus for collecting metrics 25 | 26 | ## Requirements 27 | 28 | 1. Managed Kubernetes cluster (EKS, AKS, etc...) 29 | 2. CSI driver that supports [`VolumeExpansion`](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#csi-volume-expansion) 30 | 3. A storage class with the `allowVolumeExpansion` field set to `true` 31 | 4. Only volumes with `Filesystem` mode are supported 32 | 5. A metrics collector (default: [Prometheus](https://github.com/prometheus-community/helm-charts)) 33 | 34 | ## Installation 35 | 36 | PVC Autoscaler comes with a Helm chart for easy deployment in a Kubernetes cluster. 37 | 38 | To install the PVC Autoscaler using its Helm chart, first add the repository: 39 | 40 | ```console 41 | helm repo add pvc-autoscaler https://lorenzophys.github.io/pvc-autoscaler 42 | ``` 43 | 44 | then you can install the chart by running: 45 | 46 | ```console 47 | helm install pvc-autoscaler/pvcautoscaler -n kube-system 48 | ``` 49 | 50 | Replace `` with the name you'd like to give to this Helm release. 51 | 52 | ## Usage 53 | 54 | Using `pvc-autoscaler` requires a `StorageClass` that allows volume expansion, i.e. with the `allowVolumeExpansion` field set to `true`. In case of `EKS` you can define: 55 | 56 | ```yaml 57 | apiVersion: storage.k8s.io/v1 58 | kind: StorageClass 59 | metadata: 60 | name: gp3-expandable 61 | provisioner: ebs.csi.aws.com 62 | parameters: 63 | type: gp3 64 | fsType: ext4 65 | reclaimPolicy: Delete 66 | allowVolumeExpansion: true 67 | ``` 68 | 69 | Then set up the `PersistentVolumeClaim` based on the following example: 70 | 71 | ```yaml 72 | kind: PersistentVolumeClaim 73 | apiVersion: v1 74 | metadata: 75 | name: my-pvc 76 | annotations: 77 | pvc-autoscaler.lorenzophys.io/enabled: "true" 78 | pvc-autoscaler.lorenzophys.io/threshold: 80% 79 | pvc-autoscaler.lorenzophys.io/ceiling: 20Gi 80 | pvc-autoscaler.lorenzophys.io/increase: 20% 81 | spec: 82 | accessModes: 83 | - ReadWriteOnce 84 | storageClassName: gp3-expandable 85 | volumeMode: Filesystem 86 | resources: 87 | requests: 88 | storage: 10Gi 89 | ``` 90 | 91 | * set `spec.storageClassName` to the name of the expandable `StorageClass` defined above 92 | * make sure `spec.volumeMode` is set to `Filesystem` (if you have a block storage this won't work) 93 | 94 | Then setup `metadata.annotations` this way: 95 | 96 | * to enable autoscaling set `metadata.annotations.pvc-autoscaler.lorenzophys.io/enabled` to `"true"` 97 | * the `metadata.annotations.pvc-autoscaler.lorenzophys.io/threshold` annotation fixes the volume usage above which the resizing will be triggered (default: 80%) 98 | * set how much to increase via `metadata.annotations.pvc-autoscaler.lorenzophys.io/increase` (default 20%) 99 | * to avoid infinite scaling you can set a maximum size for your volume via `metadata.annotations.pvc-autoscaler.lorenzophys.io/ceiling` (default: max size set by the volume provider) 100 | 101 | ## Contributions 102 | 103 | Contributions to PVC Autoscaler are more than welcome! Whether you want to help me improve the code, add new features, fix bugs, or improve our documentation, I would be glad to receive your pull requests and issues. 104 | 105 | ## License 106 | 107 | This project is licensed under the MIT License - see the LICENSE file for details. 108 | -------------------------------------------------------------------------------- /charts/pvc-autoscaler/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | 5 | .DS_Store 6 | # Common VCS dirs 7 | .git/ 8 | .gitignore 9 | .bzr/ 10 | .bzrignore 11 | .hg/ 12 | .hgignore 13 | .svn/ 14 | # Common backup files 15 | *.swp 16 | *.bak 17 | *.tmp 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | *.vscode 24 | -------------------------------------------------------------------------------- /charts/pvc-autoscaler/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: pvcautoscaler 3 | description: A Helm chart for PVCAutoscaler 4 | maintainers: 5 | - name: lorenzophys 6 | email: lorenzo.maffioli@gmail.com 7 | url: https://github.com/lorenzophys 8 | 9 | type: application 10 | version: 0.3.0 11 | appVersion: 0.2.1 12 | -------------------------------------------------------------------------------- /charts/pvc-autoscaler/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* Full name of the chart */}} 2 | {{- define "pvcautoscaler.fullname" -}} 3 | {{- .Chart.Name }}-{{ .Release.Name | trunc 63 | trimSuffix "-" }} 4 | {{- end }} 5 | 6 | {{/* Common labels for all resources */}} 7 | {{- define "pvcautoscaler.labels" -}} 8 | helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} 9 | app.kubernetes.io/name: {{ include "pvcautoscaler.fullname" . }} 10 | app.kubernetes.io/instance: {{ .Release.Name }} 11 | app.kubernetes.io/managed-by: {{ .Release.Service }} 12 | {{- end }} 13 | 14 | {{/* Selector labels for all resources */}} 15 | {{- define "pvcautoscaler.selectorLabels" -}} 16 | app.kubernetes.io/name: {{ include "pvcautoscaler.fullname" . }} 17 | app.kubernetes.io/instance: {{ .Release.Name }} 18 | {{- end }} 19 | -------------------------------------------------------------------------------- /charts/pvc-autoscaler/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "pvcautoscaler.fullname" . }} 5 | labels: 6 | {{- include "pvcautoscaler.labels" . | nindent 4 }} 7 | rules: 8 | - apiGroups: [""] 9 | resources: ["persistentvolumeclaims"] 10 | verbs: ["get", "list", "update", "patch"] 11 | - apiGroups: ["storage.k8s.io"] 12 | resources: ["storageclasses"] 13 | verbs: ["get", "list"] 14 | -------------------------------------------------------------------------------- /charts/pvc-autoscaler/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: {{ include "pvcautoscaler.fullname" . }} 5 | labels: 6 | {{- include "pvcautoscaler.labels" . | nindent 4 }} 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: ClusterRole 10 | name: {{ include "pvcautoscaler.fullname" . }} 11 | subjects: 12 | - kind: ServiceAccount 13 | name: {{ include "pvcautoscaler.fullname" . }} 14 | namespace: {{ .Release.Namespace }} 15 | -------------------------------------------------------------------------------- /charts/pvc-autoscaler/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "pvcautoscaler.fullname" . }} 5 | labels: 6 | {{- include "pvcautoscaler.labels" . | nindent 4 }} 7 | {{- with .Values.pvcAutoscaler.extraLabels }} 8 | {{- toYaml . | nindent 4 }} 9 | {{- end }} 10 | spec: 11 | replicas: 1 12 | selector: 13 | matchLabels: 14 | {{- include "pvcautoscaler.selectorLabels" . | nindent 6 }} 15 | template: 16 | metadata: 17 | labels: 18 | {{- include "pvcautoscaler.selectorLabels" . | nindent 8 }} 19 | spec: 20 | serviceAccountName: {{ include "pvcautoscaler.fullname" . }} 21 | containers: 22 | - name: {{ .Chart.Name }} 23 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 24 | command: 25 | - /pvc-autoscaler 26 | args: 27 | - --metrics-client={{ .Values.pvcAutoscaler.args.metricsClient }} 28 | - --metrics-client-url={{ .Values.pvcAutoscaler.args.metricsClientURL }} 29 | - --polling-interval={{ .Values.pvcAutoscaler.args.pollingInterval }} 30 | - --reconcile-timeout={{ .Values.pvcAutoscaler.args.reconcileTimeout }} 31 | - --log-level={{ .Values.pvcAutoscaler.args.logger.logLevel }} 32 | imagePullPolicy: {{ .Values.image.pullPolicy }} 33 | resources: 34 | requests: 35 | cpu: "{{ .Values.pvcAutoscaler.resources.requestCPU }}" 36 | memory: "{{ .Values.pvcAutoscaler.resources.requestMemory }}" 37 | -------------------------------------------------------------------------------- /charts/pvc-autoscaler/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ include "pvcautoscaler.fullname" . }} 5 | labels: 6 | {{- include "pvcautoscaler.labels" . | nindent 4 }} 7 | -------------------------------------------------------------------------------- /charts/pvc-autoscaler/values.yaml: -------------------------------------------------------------------------------- 1 | image: 2 | # image.repository -- pvc-autoscaler image repository to use. 3 | repository: lorenzophys/pvc-autoscaler 4 | 5 | # image.tag -- pvc-autoscaler image tag to use. 6 | tag: "latest" 7 | 8 | # image.pullPolicy -- pvc-autoscaler image pullPolicy. 9 | pullPolicy: Always 10 | 11 | pvcAutoscaler: 12 | args: 13 | # pvcAutoscaler.args.metricsClient -- Specify the metrics client to use to query volume stats. 14 | # Used as "--metrics-client" option 15 | metricsClient: "prometheus" 16 | 17 | # pvcAutoscaler.args.metricsClientURL -- Specify metrics client URL to query volume stats. 18 | # Used as "--metrics-client-url" option 19 | metricsClientURL: http://prometheus-server.monitoring.svc.cluster.local 20 | 21 | # pvcAutoscaler.args.pollingInterval -- Specify how often to check pvc stats. 22 | # Used as "--polling-interval" option 23 | pollingInterval: 30s 24 | 25 | # pvcAutoscaler.args.reconcileTimeout -- Specify the time after which the reconciliation is considered failed. 26 | # Used as "--reconcile-timeout" option 27 | reconcileTimeout: 30s 28 | 29 | logger: 30 | # pvcAutoscaler.logger.logLevel -- Specify the log level. 31 | logLevel: "INFO" 32 | 33 | # pvcAutoscaler.extraLabels -- Additional labels that will be added to pvc-autoscaler Deployment. 34 | extraLabels: {} 35 | 36 | # pvcAutoscaler.resources -- Specify resources for pvc-autoscaler deployment 37 | resources: 38 | # pvcAutoscaler.resources.requestCPU -- Request CPU resource unit in terms of millicpu 39 | requestCPU: "10m" 40 | 41 | # pvcAutoscaler.resources.requestMemory -- Request memory resource unit in terms of Mi 42 | requestMemory: "20Mi" 43 | -------------------------------------------------------------------------------- /cmd/kubeclient.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "k8s.io/client-go/kubernetes" 5 | "k8s.io/client-go/rest" 6 | ) 7 | 8 | func newKubeClient() (*kubernetes.Clientset, error) { 9 | config, err := rest.InClusterConfig() 10 | if err != nil { 11 | return nil, err 12 | } 13 | 14 | clientset, err := kubernetes.NewForConfig(config) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | return clientset, nil 20 | } 21 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | clients "github.com/lorenzophys/pvc-autoscaler/internal/metrics_clients/clients" 11 | log "github.com/sirupsen/logrus" 12 | "k8s.io/client-go/kubernetes" 13 | ) 14 | 15 | const ( 16 | PVCAutoscalerAnnotationPrefix = "pvc-autoscaler.lorenzophys.io/" 17 | PVCAutoscalerEnabledAnnotation = PVCAutoscalerAnnotationPrefix + "enabled" 18 | PVCAutoscalerThresholdAnnotation = PVCAutoscalerAnnotationPrefix + "threshold" 19 | PVCAutoscalerCeilingAnnotation = PVCAutoscalerAnnotationPrefix + "ceiling" 20 | PVCAutoscalerIncreaseAnnotation = PVCAutoscalerAnnotationPrefix + "increase" 21 | PVCAutoscalerPreviousCapacityAnnotation = PVCAutoscalerAnnotationPrefix + "previous_capacity" 22 | 23 | DefaultThreshold = "80%" 24 | DefaultIncrease = "20%" 25 | 26 | DefaultReconcileTimeOut = 1 * time.Minute 27 | DefaultPollingInterval = 30 * time.Second 28 | DefaultLogLevel = "INFO" 29 | DefaultMetricsProvider = "prometheus" 30 | ) 31 | 32 | type PVCAutoscaler struct { 33 | kubeClient kubernetes.Interface 34 | metricsClient clients.MetricsClient 35 | logger *log.Logger 36 | pollingInterval time.Duration 37 | } 38 | 39 | func main() { 40 | metricsClient := flag.String("metrics-client", DefaultMetricsProvider, "specify the metrics client to use to query volume stats") 41 | metricsClientURL := flag.String("metrics-client-url", "", "Specify the metrics client URL to use to query volume stats") 42 | pollingInterval := flag.Duration("polling-interval", DefaultPollingInterval, "specify how often to check pvc stats") 43 | reconcileTimeout := flag.Duration("reconcile-timeout", DefaultReconcileTimeOut, "specify the time after which the reconciliation is considered failed") 44 | logLevel := flag.String("log-level", DefaultLogLevel, "specify the log level") 45 | 46 | flag.Parse() 47 | 48 | var loggerLevel log.Level 49 | switch strings.ToLower(*logLevel) { 50 | case "INFO": 51 | loggerLevel = log.InfoLevel 52 | case "DEBUG": 53 | loggerLevel = log.DebugLevel 54 | default: 55 | loggerLevel = log.InfoLevel 56 | } 57 | 58 | logger := &log.Logger{ 59 | Out: os.Stderr, 60 | Formatter: new(log.JSONFormatter), 61 | Hooks: make(log.LevelHooks), 62 | Level: loggerLevel, 63 | } 64 | 65 | kubeClient, err := newKubeClient() 66 | if err != nil { 67 | logger.Fatalf("an error occurred while creating the Kubernetes client: %s", err) 68 | } 69 | logger.Info("kubernetes client ready") 70 | 71 | PVCMetricsClient, err := MetricsClientFactory(*metricsClient, *metricsClientURL) 72 | if err != nil { 73 | logger.Fatalf("metrics client error: %s", err) 74 | } 75 | 76 | logger.Infof("metrics client (%s) ready at address %s", *metricsClient, *metricsClientURL) 77 | 78 | pvcAutoscaler := &PVCAutoscaler{ 79 | kubeClient: kubeClient, 80 | metricsClient: PVCMetricsClient, 81 | logger: logger, 82 | pollingInterval: *pollingInterval, 83 | } 84 | 85 | logger.Info("pvc-autoscaler ready") 86 | 87 | ticker := time.NewTicker(pvcAutoscaler.pollingInterval) 88 | defer ticker.Stop() 89 | 90 | for range ticker.C { 91 | ctx, cancel := context.WithTimeout(context.Background(), *reconcileTimeout) 92 | 93 | err := pvcAutoscaler.reconcile(ctx) 94 | if err != nil { 95 | pvcAutoscaler.logger.Errorf("failed to reconcile: %v", err) 96 | } 97 | 98 | cancel() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /cmd/metrics.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | clients "github.com/lorenzophys/pvc-autoscaler/internal/metrics_clients/clients" 7 | "github.com/lorenzophys/pvc-autoscaler/internal/metrics_clients/prometheus" 8 | ) 9 | 10 | func MetricsClientFactory(clientName, clientUrl string) (clients.MetricsClient, error) { 11 | switch clientName { 12 | case "prometheus": 13 | prometheusClient, err := prometheus.NewPrometheusClient(clientUrl) 14 | if err != nil { 15 | return nil, err 16 | } 17 | return prometheusClient, nil 18 | default: 19 | return nil, fmt.Errorf("unknown metrics client: %s", clientName) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cmd/reconcile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math" 7 | "strconv" 8 | "time" 9 | 10 | corev1 "k8s.io/api/core/v1" 11 | "k8s.io/apimachinery/pkg/api/resource" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/types" 14 | ) 15 | 16 | func (a *PVCAutoscaler) reconcile(ctx context.Context) error { 17 | pvcl, err := getAnnotatedPVCs(ctx, a.kubeClient) 18 | if err != nil { 19 | return fmt.Errorf("could not get PersistentVolumeClaims: %w", err) 20 | } 21 | a.logger.Debugf("fetched %d annotated pvcs", pvcl.Size()) 22 | 23 | pvcsMetrics, err := a.metricsClient.FetchPVCsMetrics(ctx, time.Now()) 24 | if err != nil { 25 | a.logger.Errorf("could not fetch the PersistentVolumeClaims metrics: %v", err) 26 | return nil 27 | } 28 | a.logger.Debug("fetched pvc metrics") 29 | 30 | for _, pvc := range pvcl.Items { 31 | pvcId := fmt.Sprintf("%s/%s", pvc.Namespace, pvc.Name) 32 | a.logger.Debugf("processing pvc %s", pvcId) 33 | 34 | // Determine if the StorageClass allows volume expansion 35 | storageClassName := *pvc.Spec.StorageClassName 36 | isExpandable, err := isStorageClassExpandable(ctx, a.kubeClient, storageClassName) 37 | if err != nil { 38 | a.logger.Errorf("could not get StorageClass %s for %s: %v", storageClassName, pvcId, err) 39 | continue 40 | } 41 | if !isExpandable { 42 | a.logger.Errorf("the StorageClass %s of %s does not allow volume expansion", storageClassName, pvcId) 43 | continue 44 | } 45 | a.logger.Debugf("storageclass for %s allows volume expansion", pvcId) 46 | 47 | // Determine if pvc the meets the condition for resize 48 | err = isPVCResizable(&pvc) 49 | if err != nil { 50 | a.logger.Errorf("the PersistentVolumeClaim %s is not resizable: %v", pvcId, err) 51 | continue 52 | } 53 | a.logger.Debugf("pvc %s meets the resizing conditions", pvcId) 54 | 55 | namespacedName := types.NamespacedName{ 56 | Namespace: pvc.Namespace, 57 | Name: pvc.Name, 58 | } 59 | if _, ok := pvcsMetrics[namespacedName]; !ok { 60 | a.logger.Errorf("could not fetch the metrics for %s", pvcId) 61 | continue 62 | } 63 | a.logger.Debugf("metrics for %s received", pvcId) 64 | 65 | pvcCurrentCapacityBytes := pvcsMetrics[namespacedName].VolumeCapacityBytes 66 | 67 | threshold, err := convertPercentageToBytes(pvc.Annotations[PVCAutoscalerThresholdAnnotation], pvcCurrentCapacityBytes, DefaultThreshold) 68 | if err != nil { 69 | a.logger.Errorf("failed to convert threshold annotation for %s: %v", pvcId, err) 70 | continue 71 | } 72 | 73 | capacity, exists := pvc.Status.Capacity[corev1.ResourceStorage] 74 | if !exists { 75 | a.logger.Infof("skip %s because its capacity is not set yet", pvcId) 76 | continue 77 | } 78 | if capacity.Value() == 0 { 79 | a.logger.Infof("skip %s because its capacity is zero", pvcId) 80 | continue 81 | } 82 | 83 | increase, err := convertPercentageToBytes(pvc.Annotations[PVCAutoscalerIncreaseAnnotation], capacity.Value(), DefaultIncrease) 84 | if err != nil { 85 | a.logger.Errorf("failed to convert increase annotation for %s: %v", pvcId, err) 86 | continue 87 | } 88 | 89 | previousCapacity, exist := pvc.Annotations[PVCAutoscalerPreviousCapacityAnnotation] 90 | if exist { 91 | parsedPreviousCapacity, err := strconv.ParseInt(previousCapacity, 10, 64) 92 | if err != nil { 93 | a.logger.Errorf("failed to parse 'previous_capacity' annotation: %v", err) 94 | continue 95 | } 96 | if parsedPreviousCapacity == pvcCurrentCapacityBytes { 97 | a.logger.Infof("pvc %s is still waiting to accept the resize", pvcId) 98 | continue 99 | } 100 | } 101 | 102 | ceiling, err := getPVCStorageCeiling(&pvc) 103 | if err != nil { 104 | a.logger.Errorf("failed to fetch storage ceiling for %s: %v", pvcId, err) 105 | continue 106 | } 107 | if capacity.Cmp(ceiling) >= 0 { 108 | a.logger.Infof("volume storage limit (%s) reached for %s", ceiling.String(), pvcId) 109 | continue 110 | } 111 | 112 | currentUsedBytes := pvcsMetrics[namespacedName].VolumeUsedBytes 113 | if currentUsedBytes >= threshold { 114 | a.logger.Infof("pvc %s usage bigger than threshold", pvcId) 115 | 116 | // 1<<30 is a bit shift operation that represents 2^30, i.e. 1Gi 117 | newStorageBytes := int64(math.Ceil(float64(capacity.Value()+increase)/(1<<30))) << 30 118 | newStorage := resource.NewQuantity(newStorageBytes, resource.BinarySI) 119 | if newStorage.Cmp(ceiling) > 0 { 120 | newStorage = &ceiling 121 | } 122 | 123 | err := a.updatePVCWithNewStorageSize(ctx, &pvc, pvcCurrentCapacityBytes, newStorage) 124 | if err != nil { 125 | a.logger.Errorf("failed to resize pvc %s: %v", pvcId, err) 126 | } 127 | 128 | a.logger.Infof("pvc %s resized from %d to %d ", pvcId, capacity.Value(), newStorage.Value()) 129 | } 130 | } 131 | 132 | return nil 133 | } 134 | 135 | func (a *PVCAutoscaler) updatePVCWithNewStorageSize(ctx context.Context, pvcToResize *corev1.PersistentVolumeClaim, capacityBytes int64, newStorageBytes *resource.Quantity) error { 136 | pvcId := fmt.Sprintf("%s/%s", pvcToResize.Namespace, pvcToResize.Name) 137 | 138 | pvcToResize.Spec.Resources.Requests[corev1.ResourceStorage] = *newStorageBytes 139 | 140 | pvcToResize.Annotations[PVCAutoscalerPreviousCapacityAnnotation] = strconv.FormatInt(capacityBytes, 10) 141 | a.logger.Debugf("PVCAutoscalerPreviousCapacityAnnotation annotation written for %s ok", pvcId) 142 | 143 | _, err := a.kubeClient.CoreV1().PersistentVolumeClaims(pvcToResize.Namespace).Update(ctx, pvcToResize, metav1.UpdateOptions{}) 144 | if err != nil { 145 | return fmt.Errorf("failed to update PVC %s: %w", pvcId, err) 146 | } 147 | a.logger.Debugf("update function called and returned no error for %s ok", pvcId) 148 | 149 | return nil 150 | } 151 | -------------------------------------------------------------------------------- /cmd/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | 10 | corev1 "k8s.io/api/core/v1" 11 | "k8s.io/apimachinery/pkg/api/resource" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/client-go/kubernetes" 14 | ) 15 | 16 | func isStorageClassExpandable(ctx context.Context, kubeClient kubernetes.Interface, scName string) (bool, error) { 17 | sc, err := kubeClient.StorageV1().StorageClasses().Get(ctx, scName, metav1.GetOptions{}) 18 | if err != nil { 19 | return false, err 20 | } 21 | 22 | isExpandable := sc.AllowVolumeExpansion != nil && *sc.AllowVolumeExpansion 23 | return isExpandable, nil 24 | } 25 | 26 | func getAnnotatedPVCs(ctx context.Context, kubeClient kubernetes.Interface) (*corev1.PersistentVolumeClaimList, error) { 27 | pvcList, err := kubeClient.CoreV1().PersistentVolumeClaims("").List(ctx, metav1.ListOptions{}) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | var filteredPVCs []corev1.PersistentVolumeClaim 33 | for _, pvc := range pvcList.Items { 34 | if value, ok := pvc.Annotations[PVCAutoscalerEnabledAnnotation]; ok && value == "true" { 35 | filteredPVCs = append(filteredPVCs, pvc) 36 | } 37 | } 38 | 39 | return &corev1.PersistentVolumeClaimList{ 40 | TypeMeta: metav1.TypeMeta{ 41 | Kind: "PersistentVolumeClaimList", 42 | APIVersion: "v1", 43 | }, 44 | Items: filteredPVCs, 45 | }, nil 46 | } 47 | 48 | func getPVCStorageCeiling(pvc *corev1.PersistentVolumeClaim) (resource.Quantity, error) { 49 | if annotation, ok := pvc.Annotations[PVCAutoscalerCeilingAnnotation]; ok && annotation != "" { 50 | return resource.ParseQuantity(annotation) 51 | } 52 | 53 | return *pvc.Spec.Resources.Limits.Storage(), nil 54 | } 55 | 56 | func convertPercentageToBytes(value string, capacity int64, defaultValue string) (int64, error) { 57 | if len(value) == 0 { 58 | value = defaultValue 59 | } 60 | 61 | if strings.HasSuffix(value, "%") { 62 | perc, err := strconv.ParseFloat(strings.TrimSuffix(value, "%"), 64) 63 | if err != nil { 64 | return 0, err 65 | } 66 | if perc < 0 || perc > 100 { 67 | return 0, fmt.Errorf("annotation value %s should between 0%% and 100%%", value) 68 | } 69 | 70 | res := int64(float64(capacity) * perc / 100.0) 71 | return res, nil 72 | } else { 73 | return 0, errors.New("annotation value should be a percentage") 74 | } 75 | } 76 | 77 | func isPVCResizable(pvc *corev1.PersistentVolumeClaim) error { 78 | // Ceiling 79 | quantity, err := getPVCStorageCeiling(pvc) 80 | if err != nil { 81 | return fmt.Errorf("invalid storage ceiling in the annotation: %w", err) 82 | } 83 | if quantity.IsZero() { 84 | return errors.New("the storage ceiling is zero") 85 | } 86 | 87 | // Specs 88 | if pvc.Spec.VolumeMode != nil && *pvc.Spec.VolumeMode != corev1.PersistentVolumeFilesystem { 89 | return errors.New("the associated volume must be formatted with a filesystem") 90 | } 91 | if pvc.Status.Phase != corev1.ClaimBound { 92 | return errors.New("not bound to any pod") 93 | } 94 | 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /docs/pvc-autoscaler-architecture.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
old to new
old to new
update storage
update storage
pvc-autoscaler
pvc-autoscaler
pull volume metrics
pull volume metrics
metrics collector
metrics col...
CSI driver
CSI driver
gather annotated
pvcs
gather annotated...
receives expansion
request
receives expansion...
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lorenzophys/pvc-autoscaler 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/prometheus/client_golang v1.19.1 7 | github.com/prometheus/common v0.53.0 8 | github.com/sirupsen/logrus v1.9.3 9 | github.com/stretchr/testify v1.9.0 10 | go.uber.org/mock v0.4.0 11 | k8s.io/api v0.30.0 12 | k8s.io/apimachinery v0.30.0 13 | k8s.io/client-go v0.30.0 14 | ) 15 | 16 | require ( 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 19 | github.com/go-logr/logr v1.4.1 // indirect 20 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 21 | github.com/go-openapi/jsonreference v0.20.2 // indirect 22 | github.com/go-openapi/swag v0.22.3 // indirect 23 | github.com/gogo/protobuf v1.3.2 // indirect 24 | github.com/golang/protobuf v1.5.4 // indirect 25 | github.com/google/gnostic-models v0.6.8 // indirect 26 | github.com/google/gofuzz v1.2.0 // indirect 27 | github.com/google/uuid v1.3.0 // indirect 28 | github.com/josharian/intern v1.0.0 // indirect 29 | github.com/json-iterator/go v1.1.12 // indirect 30 | github.com/mailru/easyjson v0.7.7 // indirect 31 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 32 | github.com/modern-go/reflect2 v1.0.2 // indirect 33 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 34 | github.com/pmezard/go-difflib v1.0.0 // indirect 35 | github.com/prometheus/client_model v0.6.0 // indirect 36 | golang.org/x/net v0.23.0 // indirect 37 | golang.org/x/oauth2 v0.18.0 // indirect 38 | golang.org/x/sys v0.18.0 // indirect 39 | golang.org/x/term v0.18.0 // indirect 40 | golang.org/x/text v0.14.0 // indirect 41 | golang.org/x/time v0.3.0 // indirect 42 | google.golang.org/appengine v1.6.7 // indirect 43 | google.golang.org/protobuf v1.33.0 // indirect 44 | gopkg.in/inf.v0 v0.9.1 // indirect 45 | gopkg.in/yaml.v2 v2.4.0 // indirect 46 | gopkg.in/yaml.v3 v3.0.1 // indirect 47 | k8s.io/klog/v2 v2.120.1 // indirect 48 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect 49 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect 50 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 51 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 52 | sigs.k8s.io/yaml v1.3.0 // indirect 53 | ) 54 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 4 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 10 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 11 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 12 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 13 | github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= 14 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 15 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 16 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 17 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= 18 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 19 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 20 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 21 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 22 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 23 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 24 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 25 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 26 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 27 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 28 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 29 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 30 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 31 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 32 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 33 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 34 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= 35 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 36 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 37 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 38 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 39 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 40 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 41 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 42 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 43 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 44 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 45 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 46 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 47 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 48 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 49 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 50 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 51 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 52 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 53 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 54 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 55 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 58 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 59 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 60 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 61 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 62 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= 63 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 64 | github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= 65 | github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= 66 | github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= 67 | github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk= 68 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 69 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 70 | github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= 71 | github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= 72 | github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= 73 | github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= 74 | github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= 75 | github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= 76 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 77 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 78 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 79 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 80 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 81 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 82 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 83 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 84 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 85 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 86 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 87 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 88 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 89 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 90 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 91 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 92 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 93 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 94 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 95 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 96 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= 97 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 98 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 99 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 100 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 101 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 102 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 103 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 104 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 105 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 106 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 107 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 108 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 109 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 110 | golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= 111 | golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= 112 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 113 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 114 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 115 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 116 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 117 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 118 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 119 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 120 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 121 | golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= 122 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 123 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 124 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 125 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 126 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 127 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 128 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 129 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 130 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 131 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 132 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 133 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 134 | golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= 135 | golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= 136 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 137 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 138 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 139 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 140 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 141 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 142 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 143 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 144 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 145 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 146 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 147 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 148 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 149 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 150 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 151 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 152 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 153 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 154 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 155 | k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= 156 | k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= 157 | k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= 158 | k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= 159 | k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= 160 | k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= 161 | k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= 162 | k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 163 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= 164 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= 165 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= 166 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 167 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 168 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 169 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 170 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 171 | sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= 172 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 173 | -------------------------------------------------------------------------------- /internal/metrics_clients/clients/clients.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "k8s.io/apimachinery/pkg/types" 8 | ) 9 | 10 | type PVCMetrics struct { 11 | VolumeUsedBytes int64 12 | VolumeCapacityBytes int64 13 | } 14 | 15 | type MetricsClient interface { 16 | FetchPVCsMetrics(context.Context, time.Time) (map[types.NamespacedName]*PVCMetrics, error) 17 | } 18 | -------------------------------------------------------------------------------- /internal/metrics_clients/prometheus/mock_prometheus_api.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/prometheus/client_golang/api/prometheus/v1 (interfaces: API) 3 | 4 | // Package prometheus is a generated GoMock package. 5 | package prometheus 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | time "time" 11 | 12 | v1 "github.com/prometheus/client_golang/api/prometheus/v1" 13 | model "github.com/prometheus/common/model" 14 | gomock "go.uber.org/mock/gomock" 15 | ) 16 | 17 | // MockAPI is a mock of API interface. 18 | type MockAPI struct { 19 | ctrl *gomock.Controller 20 | recorder *MockAPIMockRecorder 21 | } 22 | 23 | // MockAPIMockRecorder is the mock recorder for MockAPI. 24 | type MockAPIMockRecorder struct { 25 | mock *MockAPI 26 | } 27 | 28 | // NewMockAPI creates a new mock instance. 29 | func NewMockAPI(ctrl *gomock.Controller) *MockAPI { 30 | mock := &MockAPI{ctrl: ctrl} 31 | mock.recorder = &MockAPIMockRecorder{mock} 32 | return mock 33 | } 34 | 35 | // EXPECT returns an object that allows the caller to indicate expected use. 36 | func (m *MockAPI) EXPECT() *MockAPIMockRecorder { 37 | return m.recorder 38 | } 39 | 40 | // AlertManagers mocks base method. 41 | func (m *MockAPI) AlertManagers(arg0 context.Context) (v1.AlertManagersResult, error) { 42 | m.ctrl.T.Helper() 43 | ret := m.ctrl.Call(m, "AlertManagers", arg0) 44 | ret0, _ := ret[0].(v1.AlertManagersResult) 45 | ret1, _ := ret[1].(error) 46 | return ret0, ret1 47 | } 48 | 49 | // AlertManagers indicates an expected call of AlertManagers. 50 | func (mr *MockAPIMockRecorder) AlertManagers(arg0 interface{}) *gomock.Call { 51 | mr.mock.ctrl.T.Helper() 52 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AlertManagers", reflect.TypeOf((*MockAPI)(nil).AlertManagers), arg0) 53 | } 54 | 55 | // Alerts mocks base method. 56 | func (m *MockAPI) Alerts(arg0 context.Context) (v1.AlertsResult, error) { 57 | m.ctrl.T.Helper() 58 | ret := m.ctrl.Call(m, "Alerts", arg0) 59 | ret0, _ := ret[0].(v1.AlertsResult) 60 | ret1, _ := ret[1].(error) 61 | return ret0, ret1 62 | } 63 | 64 | // Alerts indicates an expected call of Alerts. 65 | func (mr *MockAPIMockRecorder) Alerts(arg0 interface{}) *gomock.Call { 66 | mr.mock.ctrl.T.Helper() 67 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Alerts", reflect.TypeOf((*MockAPI)(nil).Alerts), arg0) 68 | } 69 | 70 | // Buildinfo mocks base method. 71 | func (m *MockAPI) Buildinfo(arg0 context.Context) (v1.BuildinfoResult, error) { 72 | m.ctrl.T.Helper() 73 | ret := m.ctrl.Call(m, "Buildinfo", arg0) 74 | ret0, _ := ret[0].(v1.BuildinfoResult) 75 | ret1, _ := ret[1].(error) 76 | return ret0, ret1 77 | } 78 | 79 | // Buildinfo indicates an expected call of Buildinfo. 80 | func (mr *MockAPIMockRecorder) Buildinfo(arg0 interface{}) *gomock.Call { 81 | mr.mock.ctrl.T.Helper() 82 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Buildinfo", reflect.TypeOf((*MockAPI)(nil).Buildinfo), arg0) 83 | } 84 | 85 | // CleanTombstones mocks base method. 86 | func (m *MockAPI) CleanTombstones(arg0 context.Context) error { 87 | m.ctrl.T.Helper() 88 | ret := m.ctrl.Call(m, "CleanTombstones", arg0) 89 | ret0, _ := ret[0].(error) 90 | return ret0 91 | } 92 | 93 | // CleanTombstones indicates an expected call of CleanTombstones. 94 | func (mr *MockAPIMockRecorder) CleanTombstones(arg0 interface{}) *gomock.Call { 95 | mr.mock.ctrl.T.Helper() 96 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanTombstones", reflect.TypeOf((*MockAPI)(nil).CleanTombstones), arg0) 97 | } 98 | 99 | // Config mocks base method. 100 | func (m *MockAPI) Config(arg0 context.Context) (v1.ConfigResult, error) { 101 | m.ctrl.T.Helper() 102 | ret := m.ctrl.Call(m, "Config", arg0) 103 | ret0, _ := ret[0].(v1.ConfigResult) 104 | ret1, _ := ret[1].(error) 105 | return ret0, ret1 106 | } 107 | 108 | // Config indicates an expected call of Config. 109 | func (mr *MockAPIMockRecorder) Config(arg0 interface{}) *gomock.Call { 110 | mr.mock.ctrl.T.Helper() 111 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Config", reflect.TypeOf((*MockAPI)(nil).Config), arg0) 112 | } 113 | 114 | // DeleteSeries mocks base method. 115 | func (m *MockAPI) DeleteSeries(arg0 context.Context, arg1 []string, arg2, arg3 time.Time) error { 116 | m.ctrl.T.Helper() 117 | ret := m.ctrl.Call(m, "DeleteSeries", arg0, arg1, arg2, arg3) 118 | ret0, _ := ret[0].(error) 119 | return ret0 120 | } 121 | 122 | // DeleteSeries indicates an expected call of DeleteSeries. 123 | func (mr *MockAPIMockRecorder) DeleteSeries(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { 124 | mr.mock.ctrl.T.Helper() 125 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSeries", reflect.TypeOf((*MockAPI)(nil).DeleteSeries), arg0, arg1, arg2, arg3) 126 | } 127 | 128 | // Flags mocks base method. 129 | func (m *MockAPI) Flags(arg0 context.Context) (v1.FlagsResult, error) { 130 | m.ctrl.T.Helper() 131 | ret := m.ctrl.Call(m, "Flags", arg0) 132 | ret0, _ := ret[0].(v1.FlagsResult) 133 | ret1, _ := ret[1].(error) 134 | return ret0, ret1 135 | } 136 | 137 | // Flags indicates an expected call of Flags. 138 | func (mr *MockAPIMockRecorder) Flags(arg0 interface{}) *gomock.Call { 139 | mr.mock.ctrl.T.Helper() 140 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Flags", reflect.TypeOf((*MockAPI)(nil).Flags), arg0) 141 | } 142 | 143 | // LabelNames mocks base method. 144 | func (m *MockAPI) LabelNames(arg0 context.Context, arg1 []string, arg2, arg3 time.Time) ([]string, v1.Warnings, error) { 145 | m.ctrl.T.Helper() 146 | ret := m.ctrl.Call(m, "LabelNames", arg0, arg1, arg2, arg3) 147 | ret0, _ := ret[0].([]string) 148 | ret1, _ := ret[1].(v1.Warnings) 149 | ret2, _ := ret[2].(error) 150 | return ret0, ret1, ret2 151 | } 152 | 153 | // LabelNames indicates an expected call of LabelNames. 154 | func (mr *MockAPIMockRecorder) LabelNames(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { 155 | mr.mock.ctrl.T.Helper() 156 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LabelNames", reflect.TypeOf((*MockAPI)(nil).LabelNames), arg0, arg1, arg2, arg3) 157 | } 158 | 159 | // LabelValues mocks base method. 160 | func (m *MockAPI) LabelValues(arg0 context.Context, arg1 string, arg2 []string, arg3, arg4 time.Time) (model.LabelValues, v1.Warnings, error) { 161 | m.ctrl.T.Helper() 162 | ret := m.ctrl.Call(m, "LabelValues", arg0, arg1, arg2, arg3, arg4) 163 | ret0, _ := ret[0].(model.LabelValues) 164 | ret1, _ := ret[1].(v1.Warnings) 165 | ret2, _ := ret[2].(error) 166 | return ret0, ret1, ret2 167 | } 168 | 169 | // LabelValues indicates an expected call of LabelValues. 170 | func (mr *MockAPIMockRecorder) LabelValues(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { 171 | mr.mock.ctrl.T.Helper() 172 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LabelValues", reflect.TypeOf((*MockAPI)(nil).LabelValues), arg0, arg1, arg2, arg3, arg4) 173 | } 174 | 175 | // Metadata mocks base method. 176 | func (m *MockAPI) Metadata(arg0 context.Context, arg1, arg2 string) (map[string][]v1.Metadata, error) { 177 | m.ctrl.T.Helper() 178 | ret := m.ctrl.Call(m, "Metadata", arg0, arg1, arg2) 179 | ret0, _ := ret[0].(map[string][]v1.Metadata) 180 | ret1, _ := ret[1].(error) 181 | return ret0, ret1 182 | } 183 | 184 | // Metadata indicates an expected call of Metadata. 185 | func (mr *MockAPIMockRecorder) Metadata(arg0, arg1, arg2 interface{}) *gomock.Call { 186 | mr.mock.ctrl.T.Helper() 187 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Metadata", reflect.TypeOf((*MockAPI)(nil).Metadata), arg0, arg1, arg2) 188 | } 189 | 190 | // Query mocks base method. 191 | func (m *MockAPI) Query(arg0 context.Context, arg1 string, arg2 time.Time, arg3 ...v1.Option) (model.Value, v1.Warnings, error) { 192 | m.ctrl.T.Helper() 193 | varargs := []interface{}{arg0, arg1, arg2} 194 | for _, a := range arg3 { 195 | varargs = append(varargs, a) 196 | } 197 | ret := m.ctrl.Call(m, "Query", varargs...) 198 | ret0, _ := ret[0].(model.Value) 199 | ret1, _ := ret[1].(v1.Warnings) 200 | ret2, _ := ret[2].(error) 201 | return ret0, ret1, ret2 202 | } 203 | 204 | // Query indicates an expected call of Query. 205 | func (mr *MockAPIMockRecorder) Query(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { 206 | mr.mock.ctrl.T.Helper() 207 | varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) 208 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockAPI)(nil).Query), varargs...) 209 | } 210 | 211 | // QueryExemplars mocks base method. 212 | func (m *MockAPI) QueryExemplars(arg0 context.Context, arg1 string, arg2, arg3 time.Time) ([]v1.ExemplarQueryResult, error) { 213 | m.ctrl.T.Helper() 214 | ret := m.ctrl.Call(m, "QueryExemplars", arg0, arg1, arg2, arg3) 215 | ret0, _ := ret[0].([]v1.ExemplarQueryResult) 216 | ret1, _ := ret[1].(error) 217 | return ret0, ret1 218 | } 219 | 220 | // QueryExemplars indicates an expected call of QueryExemplars. 221 | func (mr *MockAPIMockRecorder) QueryExemplars(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { 222 | mr.mock.ctrl.T.Helper() 223 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryExemplars", reflect.TypeOf((*MockAPI)(nil).QueryExemplars), arg0, arg1, arg2, arg3) 224 | } 225 | 226 | // QueryRange mocks base method. 227 | func (m *MockAPI) QueryRange(arg0 context.Context, arg1 string, arg2 v1.Range, arg3 ...v1.Option) (model.Value, v1.Warnings, error) { 228 | m.ctrl.T.Helper() 229 | varargs := []interface{}{arg0, arg1, arg2} 230 | for _, a := range arg3 { 231 | varargs = append(varargs, a) 232 | } 233 | ret := m.ctrl.Call(m, "QueryRange", varargs...) 234 | ret0, _ := ret[0].(model.Value) 235 | ret1, _ := ret[1].(v1.Warnings) 236 | ret2, _ := ret[2].(error) 237 | return ret0, ret1, ret2 238 | } 239 | 240 | // QueryRange indicates an expected call of QueryRange. 241 | func (mr *MockAPIMockRecorder) QueryRange(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { 242 | mr.mock.ctrl.T.Helper() 243 | varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) 244 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRange", reflect.TypeOf((*MockAPI)(nil).QueryRange), varargs...) 245 | } 246 | 247 | // Rules mocks base method. 248 | func (m *MockAPI) Rules(arg0 context.Context) (v1.RulesResult, error) { 249 | m.ctrl.T.Helper() 250 | ret := m.ctrl.Call(m, "Rules", arg0) 251 | ret0, _ := ret[0].(v1.RulesResult) 252 | ret1, _ := ret[1].(error) 253 | return ret0, ret1 254 | } 255 | 256 | // Rules indicates an expected call of Rules. 257 | func (mr *MockAPIMockRecorder) Rules(arg0 interface{}) *gomock.Call { 258 | mr.mock.ctrl.T.Helper() 259 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rules", reflect.TypeOf((*MockAPI)(nil).Rules), arg0) 260 | } 261 | 262 | // Runtimeinfo mocks base method. 263 | func (m *MockAPI) Runtimeinfo(arg0 context.Context) (v1.RuntimeinfoResult, error) { 264 | m.ctrl.T.Helper() 265 | ret := m.ctrl.Call(m, "Runtimeinfo", arg0) 266 | ret0, _ := ret[0].(v1.RuntimeinfoResult) 267 | ret1, _ := ret[1].(error) 268 | return ret0, ret1 269 | } 270 | 271 | // Runtimeinfo indicates an expected call of Runtimeinfo. 272 | func (mr *MockAPIMockRecorder) Runtimeinfo(arg0 interface{}) *gomock.Call { 273 | mr.mock.ctrl.T.Helper() 274 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Runtimeinfo", reflect.TypeOf((*MockAPI)(nil).Runtimeinfo), arg0) 275 | } 276 | 277 | // Series mocks base method. 278 | func (m *MockAPI) Series(arg0 context.Context, arg1 []string, arg2, arg3 time.Time) ([]model.LabelSet, v1.Warnings, error) { 279 | m.ctrl.T.Helper() 280 | ret := m.ctrl.Call(m, "Series", arg0, arg1, arg2, arg3) 281 | ret0, _ := ret[0].([]model.LabelSet) 282 | ret1, _ := ret[1].(v1.Warnings) 283 | ret2, _ := ret[2].(error) 284 | return ret0, ret1, ret2 285 | } 286 | 287 | // Series indicates an expected call of Series. 288 | func (mr *MockAPIMockRecorder) Series(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { 289 | mr.mock.ctrl.T.Helper() 290 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Series", reflect.TypeOf((*MockAPI)(nil).Series), arg0, arg1, arg2, arg3) 291 | } 292 | 293 | // Snapshot mocks base method. 294 | func (m *MockAPI) Snapshot(arg0 context.Context, arg1 bool) (v1.SnapshotResult, error) { 295 | m.ctrl.T.Helper() 296 | ret := m.ctrl.Call(m, "Snapshot", arg0, arg1) 297 | ret0, _ := ret[0].(v1.SnapshotResult) 298 | ret1, _ := ret[1].(error) 299 | return ret0, ret1 300 | } 301 | 302 | // Snapshot indicates an expected call of Snapshot. 303 | func (mr *MockAPIMockRecorder) Snapshot(arg0, arg1 interface{}) *gomock.Call { 304 | mr.mock.ctrl.T.Helper() 305 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Snapshot", reflect.TypeOf((*MockAPI)(nil).Snapshot), arg0, arg1) 306 | } 307 | 308 | // TSDB mocks base method. 309 | func (m *MockAPI) TSDB(arg0 context.Context) (v1.TSDBResult, error) { 310 | m.ctrl.T.Helper() 311 | ret := m.ctrl.Call(m, "TSDB", arg0) 312 | ret0, _ := ret[0].(v1.TSDBResult) 313 | ret1, _ := ret[1].(error) 314 | return ret0, ret1 315 | } 316 | 317 | // TSDB indicates an expected call of TSDB. 318 | func (mr *MockAPIMockRecorder) TSDB(arg0 interface{}) *gomock.Call { 319 | mr.mock.ctrl.T.Helper() 320 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSDB", reflect.TypeOf((*MockAPI)(nil).TSDB), arg0) 321 | } 322 | 323 | // Targets mocks base method. 324 | func (m *MockAPI) Targets(arg0 context.Context) (v1.TargetsResult, error) { 325 | m.ctrl.T.Helper() 326 | ret := m.ctrl.Call(m, "Targets", arg0) 327 | ret0, _ := ret[0].(v1.TargetsResult) 328 | ret1, _ := ret[1].(error) 329 | return ret0, ret1 330 | } 331 | 332 | // Targets indicates an expected call of Targets. 333 | func (mr *MockAPIMockRecorder) Targets(arg0 interface{}) *gomock.Call { 334 | mr.mock.ctrl.T.Helper() 335 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Targets", reflect.TypeOf((*MockAPI)(nil).Targets), arg0) 336 | } 337 | 338 | // TargetsMetadata mocks base method. 339 | func (m *MockAPI) TargetsMetadata(arg0 context.Context, arg1, arg2, arg3 string) ([]v1.MetricMetadata, error) { 340 | m.ctrl.T.Helper() 341 | ret := m.ctrl.Call(m, "TargetsMetadata", arg0, arg1, arg2, arg3) 342 | ret0, _ := ret[0].([]v1.MetricMetadata) 343 | ret1, _ := ret[1].(error) 344 | return ret0, ret1 345 | } 346 | 347 | // TargetsMetadata indicates an expected call of TargetsMetadata. 348 | func (mr *MockAPIMockRecorder) TargetsMetadata(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { 349 | mr.mock.ctrl.T.Helper() 350 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TargetsMetadata", reflect.TypeOf((*MockAPI)(nil).TargetsMetadata), arg0, arg1, arg2, arg3) 351 | } 352 | 353 | // WalReplay mocks base method. 354 | func (m *MockAPI) WalReplay(arg0 context.Context) (v1.WalReplayStatus, error) { 355 | m.ctrl.T.Helper() 356 | ret := m.ctrl.Call(m, "WalReplay", arg0) 357 | ret0, _ := ret[0].(v1.WalReplayStatus) 358 | ret1, _ := ret[1].(error) 359 | return ret0, ret1 360 | } 361 | 362 | // WalReplay indicates an expected call of WalReplay. 363 | func (mr *MockAPIMockRecorder) WalReplay(arg0 interface{}) *gomock.Call { 364 | mr.mock.ctrl.T.Helper() 365 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WalReplay", reflect.TypeOf((*MockAPI)(nil).WalReplay), arg0) 366 | } 367 | -------------------------------------------------------------------------------- /internal/metrics_clients/prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | clients "github.com/lorenzophys/pvc-autoscaler/internal/metrics_clients/clients" 9 | prometheusApi "github.com/prometheus/client_golang/api" 10 | prometheusv1 "github.com/prometheus/client_golang/api/prometheus/v1" 11 | "github.com/prometheus/common/model" 12 | "k8s.io/apimachinery/pkg/types" 13 | ) 14 | 15 | const ( 16 | usedBytesQuery = "kubelet_volume_stats_used_bytes" 17 | capacityBytesQuery = "kubelet_volume_stats_capacity_bytes" 18 | ) 19 | 20 | type PrometheusClient struct { 21 | prometheusAPI prometheusv1.API 22 | } 23 | 24 | func NewPrometheusClient(url string) (clients.MetricsClient, error) { 25 | client, err := prometheusApi.NewClient(prometheusApi.Config{ 26 | Address: url, 27 | }) 28 | if err != nil { 29 | return nil, err 30 | } 31 | v1api := prometheusv1.NewAPI(client) 32 | 33 | return &PrometheusClient{ 34 | prometheusAPI: v1api, 35 | }, nil 36 | } 37 | 38 | func (c *PrometheusClient) FetchPVCsMetrics(ctx context.Context, when time.Time) (map[types.NamespacedName]*clients.PVCMetrics, error) { 39 | volumeStats := make(map[types.NamespacedName]*clients.PVCMetrics) 40 | 41 | usedBytes, err := c.getMetricValues(ctx, usedBytesQuery, when) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | capacityBytes, err := c.getMetricValues(ctx, capacityBytesQuery, when) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | for key, val := range usedBytes { 52 | pvcMetrics := &clients.PVCMetrics{VolumeUsedBytes: val} 53 | if cb, ok := capacityBytes[key]; ok { 54 | pvcMetrics.VolumeCapacityBytes = cb 55 | } else { 56 | continue 57 | } 58 | 59 | volumeStats[key] = pvcMetrics 60 | } 61 | 62 | return volumeStats, nil 63 | } 64 | 65 | func (c *PrometheusClient) getMetricValues(ctx context.Context, query string, time time.Time) (map[types.NamespacedName]int64, error) { 66 | res, _, err := c.prometheusAPI.Query(ctx, query, time) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | if res.Type() != model.ValVector { 72 | return nil, fmt.Errorf("unknown response type: %s", res.Type().String()) 73 | } 74 | resultMap := make(map[types.NamespacedName]int64) 75 | vec := res.(model.Vector) 76 | for _, val := range vec { 77 | nn := types.NamespacedName{ 78 | Namespace: string(val.Metric["namespace"]), 79 | Name: string(val.Metric["persistentvolumeclaim"]), 80 | } 81 | resultMap[nn] = int64(val.Value) 82 | } 83 | return resultMap, nil 84 | } 85 | -------------------------------------------------------------------------------- /internal/metrics_clients/prometheus/prometheus_test.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | "time" 10 | 11 | clients "github.com/lorenzophys/pvc-autoscaler/internal/metrics_clients/clients" 12 | prometheusv1 "github.com/prometheus/client_golang/api/prometheus/v1" 13 | prometheusmodel "github.com/prometheus/common/model" 14 | "github.com/stretchr/testify/assert" 15 | "go.uber.org/mock/gomock" 16 | "k8s.io/apimachinery/pkg/types" 17 | ) 18 | 19 | type MockPrometheusAPI struct { 20 | } 21 | 22 | func TestGetMetricValues(t *testing.T) { 23 | t.Run("server not found", func(t *testing.T) { 24 | ts := httptest.NewServer(http.HandlerFunc(http.NotFound)) 25 | defer ts.Close() 26 | 27 | // If 404 the client should be created 28 | client, err := NewPrometheusClient(ts.URL) 29 | assert.NoError(t, err) 30 | 31 | // but the metrics obviously cannot be fetched 32 | _, err = client.FetchPVCsMetrics(context.TODO(), time.Time{}) 33 | assert.Error(t, err) 34 | 35 | }) 36 | 37 | t.Run("good query", func(t *testing.T) { 38 | ctrl := gomock.NewController(t) 39 | defer ctrl.Finish() 40 | 41 | mockAPI := NewMockAPI(ctrl) 42 | 43 | client := &PrometheusClient{ 44 | prometheusAPI: mockAPI, 45 | } 46 | 47 | mockReturn := prometheusmodel.Vector{ 48 | &prometheusmodel.Sample{ 49 | Metric: prometheusmodel.Metric{"namespace": "default", "persistentvolumeclaim": "mypvc"}, 50 | Value: 100, 51 | Timestamp: prometheusmodel.TimeFromUnix(123), 52 | }, 53 | } 54 | expectedResult := map[types.NamespacedName]int64{ 55 | {Namespace: "default", Name: "mypvc"}: 100, 56 | } 57 | 58 | mockAPI. 59 | EXPECT(). 60 | Query(context.TODO(), "good_query", time.Time{}). 61 | Return(mockReturn, nil, nil). 62 | AnyTimes() 63 | 64 | result, err := client.getMetricValues(context.TODO(), "good_query", time.Time{}) 65 | 66 | assert.NoError(t, err) 67 | assert.Equal(t, expectedResult, result) 68 | }) 69 | 70 | t.Run("bad query", func(t *testing.T) { 71 | ctrl := gomock.NewController(t) 72 | defer ctrl.Finish() 73 | 74 | mockAPI := NewMockAPI(ctrl) 75 | 76 | client := &PrometheusClient{ 77 | prometheusAPI: mockAPI, 78 | } 79 | 80 | mockAPI. 81 | EXPECT(). 82 | Query(context.TODO(), "bad_query", time.Time{}). 83 | Return(nil, nil, errors.New("generic error")). 84 | AnyTimes() 85 | 86 | _, err := client.getMetricValues(context.TODO(), "bad_query", time.Time{}) 87 | 88 | assert.Error(t, err) 89 | 90 | }) 91 | } 92 | 93 | func TestFetchPVCsMetrics(t *testing.T) { 94 | t.Run("everything fine", func(t *testing.T) { 95 | ctrl := gomock.NewController(t) 96 | defer ctrl.Finish() 97 | 98 | mockAPI := NewMockAPI(ctrl) 99 | 100 | client := &PrometheusClient{ 101 | prometheusAPI: mockAPI, 102 | } 103 | 104 | mockUsedBytesQuery := prometheusmodel.Vector{ 105 | &prometheusmodel.Sample{ 106 | Metric: prometheusmodel.Metric{"namespace": "default", "persistentvolumeclaim": "mypvc"}, 107 | Value: 80, 108 | Timestamp: prometheusmodel.TimeFromUnix(123), 109 | }, 110 | } 111 | 112 | mockCapacityBytesQuery := prometheusmodel.Vector{ 113 | &prometheusmodel.Sample{ 114 | Metric: prometheusmodel.Metric{"namespace": "default", "persistentvolumeclaim": "mypvc"}, 115 | Value: 100, 116 | Timestamp: prometheusmodel.TimeFromUnix(123), 117 | }, 118 | } 119 | 120 | expectedPVCMetric := &clients.PVCMetrics{ 121 | VolumeUsedBytes: 80, 122 | VolumeCapacityBytes: 100, 123 | } 124 | 125 | expectedResult := map[types.NamespacedName]*clients.PVCMetrics{ 126 | {Namespace: "default", Name: "mypvc"}: expectedPVCMetric, 127 | } 128 | 129 | mockAPI. 130 | EXPECT(). 131 | Query(context.TODO(), gomock.Any(), time.Time{}). 132 | DoAndReturn(func(ctx context.Context, query string, time time.Time, args ...any) (prometheusmodel.Value, prometheusv1.Warnings, error) { 133 | if query == usedBytesQuery { 134 | return mockUsedBytesQuery, nil, nil 135 | } else { 136 | return mockCapacityBytesQuery, nil, nil 137 | } 138 | }).Times(2) 139 | 140 | result, err := client.FetchPVCsMetrics(context.TODO(), time.Time{}) 141 | 142 | assert.NoError(t, err) 143 | assert.Equal(t, expectedResult, result) 144 | }) 145 | 146 | } 147 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | --------------------------------------------------------------------------------