├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── enhancement.md │ └── support.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yaml └── workflows │ ├── gh-workflow-approve.yaml │ ├── lint-test-chart.yaml │ ├── release-chart.yaml │ └── release.yaml ├── .gitignore ├── .golangci.yml ├── CONTRIBUTING.md ├── Dockerfile ├── FAQ.md ├── KNOWN_ISSUES.md ├── LICENSE ├── Makefile ├── OWNERS ├── README.md ├── RELEASE.md ├── SECURITY.md ├── SECURITY_CONTACTS ├── charts ├── OWNERS └── metrics-server │ ├── .helmignore │ ├── CHANGELOG.md │ ├── Chart.yaml │ ├── README.md │ ├── ci │ ├── ci-values.yaml │ ├── tls-certManager-values.yaml │ ├── tls-existingSecret-values.yaml │ └── tls-helm-values.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── apiservice.yaml │ ├── certificate.yaml │ ├── clusterrole-aggregated-reader.yaml │ ├── clusterrole-nanny.yaml │ ├── clusterrole.yaml │ ├── clusterrolebinding-auth-delegator.yaml │ ├── clusterrolebinding-nanny.yaml │ ├── clusterrolebinding.yaml │ ├── configmaps-nanny.yaml │ ├── deployment.yaml │ ├── pdb.yaml │ ├── psp.yaml │ ├── role-nanny.yaml │ ├── rolebinding-nanny.yaml │ ├── rolebinding.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── servicemonitor.yaml │ └── values.yaml ├── cloudbuild.yaml ├── cmd └── metrics-server │ ├── app │ ├── options │ │ ├── kubelet_client.go │ │ ├── kubelet_client_test.go │ │ ├── options.go │ │ └── options_test.go │ └── start.go │ └── metrics-server.go ├── code-of-conduct.md ├── docs └── command-line-flags.txt ├── go.mod ├── go.sum ├── manifests ├── base │ ├── apiservice.yaml │ ├── deployment.yaml │ ├── kustomization.yaml │ ├── rbac.yaml │ └── service.yaml ├── components │ ├── autoscale │ │ ├── kustomization.yaml │ │ ├── patch.yaml │ │ └── rbac.yaml │ ├── high-availability-1.21+ │ │ ├── kustomization.yaml │ │ └── patch-pdb-version.yaml │ ├── high-availability │ │ ├── kustomization.yaml │ │ ├── patch.yaml │ │ └── pdb.yaml │ ├── release │ │ └── kustomization.yaml │ └── test │ │ └── kustomization.yaml └── overlays │ ├── release-ha-1.21+ │ └── kustomization.yaml │ ├── release-ha │ └── kustomization.yaml │ ├── release │ └── kustomization.yaml │ ├── test-ha │ └── kustomization.yaml │ └── test │ └── kustomization.yaml ├── pkg ├── api │ ├── filter.go │ ├── generated │ │ └── openapi │ │ │ ├── doc.go │ │ │ └── zz_generated.openapi.go │ ├── install.go │ ├── interfaces.go │ ├── monitoring.go │ ├── node.go │ ├── node_test.go │ ├── pod.go │ ├── pod_test.go │ ├── table.go │ ├── table_test.go │ └── time.go ├── scraper │ ├── client │ │ ├── configs.go │ │ ├── interface.go │ │ └── resource │ │ │ ├── client.go │ │ │ ├── client_test.go │ │ │ ├── decode.go │ │ │ └── decode_test.go │ ├── interface.go │ ├── scraper.go │ └── scraper_test.go ├── server │ ├── config.go │ ├── health.go │ ├── informer.go │ ├── metrics.go │ ├── server.go │ └── server_test.go ├── storage │ ├── interface.go │ ├── main_test.go │ ├── monitoring.go │ ├── node.go │ ├── node_test.go │ ├── pod.go │ ├── pod_test.go │ ├── storage.go │ ├── storage_benchmark_test.go │ ├── storage_test.go │ ├── types.go │ └── types_test.go └── utils │ ├── address_resolver.go │ ├── monitoring.go │ └── monitoring_test.go ├── scripts ├── boilerplate.go.txt ├── go.mod ├── go.sum └── tools.go ├── skaffold.yaml └── test ├── chart-values.yaml ├── e2e_test.go ├── kind-config-with-sidecar-containers.yaml ├── kind-config.yaml ├── test-e2e.sh └── test-image.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | /_output 2 | /metrics-server 3 | /.git -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug encountered while operating Metrics Server 4 | 5 | --- 6 | 7 | 19 | 20 | **What happened**: 21 | 22 | **What you expected to happen**: 23 | 24 | **Anything else we need to know?**: 25 | 26 | **Environment**: 27 | 28 | - Kubernetes distribution (GKE, EKS, Kubeadm, the hard way, etc.): 29 | - Container Network Setup (flannel, calico, etc.): 30 | - Kubernetes version (use `kubectl version`): 31 | 32 | - Metrics Server manifest 33 | 34 |
35 | spoiler for Metrics Server manifest: 36 | 37 | 38 | 39 |
40 | 41 | - Kubelet config: 42 | 43 |
44 | spoiler for Kubelet config: 45 | 46 | 47 | 48 |
49 | 50 | - Metrics server logs: 51 | 52 |
53 | spoiler for Metrics Server logs: 54 | 55 | 56 | 57 |
58 | 59 | - Status of Metrics API: 60 | 61 |
62 | spolier for Status of Metrics API: 63 | 64 | ```sh 65 | kubectl describe apiservice v1beta1.metrics.k8s.io 66 | ``` 67 | 68 | 69 | 70 |
71 | 72 | 73 | /kind bug 74 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement Request 3 | about: Suggest an enhancement to the Metrics Server 4 | 5 | --- 6 | 7 | 8 | **What would you like to be added**: 9 | 10 | **Why is this needed**: 11 | 12 | 13 | /kind feature -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support Request 3 | about: Support request or question relating to Metrics Server 4 | 5 | --- 6 | 7 | 17 | 18 | 19 | 20 | /kind support -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | **What this PR does / why we need it**: 9 | 10 | **Which issue(s) this PR fixes** *(optional, in `fixes #(, fixes #, ...)` format, will close the issue(s) when PR gets merged)*: 11 | Fixes # 12 | 13 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: docker 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | - package-ecosystem: gomod 12 | directory: / 13 | schedule: 14 | interval: weekly 15 | groups: 16 | gomod-dependencies: 17 | patterns: 18 | - "*" 19 | - package-ecosystem: gomod 20 | directory: /scripts 21 | schedule: 22 | interval: weekly 23 | groups: 24 | gomod-dependencies: 25 | patterns: 26 | - "*" 27 | -------------------------------------------------------------------------------- /.github/workflows/gh-workflow-approve.yaml: -------------------------------------------------------------------------------- 1 | name: Approve GH Workflows 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - labeled 7 | - synchronize 8 | branches: 9 | - master 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | approve: 16 | name: Approve ok-to-test 17 | if: contains(github.event.pull_request.labels.*.name, 'ok-to-test') 18 | runs-on: ubuntu-latest 19 | permissions: 20 | actions: write 21 | steps: 22 | - name: Update PR 23 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 24 | continue-on-error: true 25 | with: 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | debug: ${{ secrets.ACTIONS_RUNNER_DEBUG == 'true' }} 28 | script: | 29 | const result = await github.rest.actions.listWorkflowRunsForRepo({ 30 | owner: context.repo.owner, 31 | repo: context.repo.repo, 32 | event: "pull_request", 33 | status: "action_required", 34 | head_sha: context.payload.pull_request.head.sha, 35 | per_page: 100 36 | }); 37 | 38 | for (var run of result.data.workflow_runs) { 39 | await github.rest.actions.approveWorkflowRun({ 40 | owner: context.repo.owner, 41 | repo: context.repo.repo, 42 | run_id: run.id 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/lint-test-chart.yaml: -------------------------------------------------------------------------------- 1 | name: Lint and Test Chart 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - .github/workflows/lint-test-chart.yaml 7 | - "charts/metrics-server/**" 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | lint-test: 14 | name: Lint & Test 15 | if: github.repository == 'kubernetes-sigs/metrics-server' 16 | runs-on: ubuntu-latest 17 | defaults: 18 | run: 19 | shell: bash 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Set-up Python 27 | uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 28 | with: 29 | python-version: "3.x" 30 | 31 | - name: Set-up Helm 32 | uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0 33 | with: 34 | token: ${{ secrets.GITHUB_TOKEN }} 35 | version: latest 36 | 37 | - name: Set-up chart-testing 38 | uses: helm/chart-testing-action@e6669bcd63d7cb57cb4380c33043eebe5d111992 # v2.6.1 39 | 40 | - name: Check for changes 41 | id: changes 42 | run: | 43 | changed="$(ct list-changed)" 44 | if [[ -n "${changed}" ]] 45 | then 46 | echo "changed=true" >> "${GITHUB_OUTPUT}" 47 | else 48 | echo "changed=false" >> "${GITHUB_OUTPUT}" 49 | fi 50 | 51 | - name: Get chart version 52 | id: chart_version 53 | if: steps.changes.outputs.changed == 'true' 54 | uses: mikefarah/yq@bbdd97482f2d439126582a59689eb1c855944955 # v4.44.3 55 | with: 56 | cmd: yq eval '.version' './charts/metrics-server/Chart.yaml' 57 | 58 | - name: Get changelog entry 59 | if: steps.changes.outputs.changed == 'true' 60 | uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2.2.3 61 | with: 62 | path: charts/metrics-server/CHANGELOG.md 63 | version: ${{ steps.chart_version.outputs.result }} 64 | 65 | - name: Set-up Artifact Hub CLI 66 | if: steps.changes.outputs.changed == 'true' 67 | uses: action-stars/install-tool-from-github-release@ece2623611b240002e0dd73a0d685505733122f6 # v0.2.4 68 | with: 69 | github_token: ${{ github.token }} 70 | owner: artifacthub 71 | repository: hub 72 | name: ah 73 | check_command: ah version 74 | version: latest 75 | 76 | - name: Run Artifact Hub lint 77 | if: steps.changes.outputs.changed == 'true' 78 | run: ah lint --kind helm || exit 1 79 | 80 | - name: Run chart-testing lint 81 | if: steps.changes.outputs.changed == 'true' 82 | run: ct lint --check-version-increment=false 83 | 84 | - name: Create Kind cluster 85 | if: steps.changes.outputs.changed == 'true' 86 | uses: helm/kind-action@0025e74a8c7512023d06dc019c617aa3cf561fde # v1.10.0 87 | with: 88 | wait: 120s 89 | 90 | - name: Install cert-manager dependency 91 | if: steps.changes.outputs.changed == 'true' 92 | run: | 93 | helm repo add jetstack https://charts.jetstack.io 94 | helm install cert-manager jetstack/cert-manager \ 95 | --namespace cert-manager \ 96 | --create-namespace \ 97 | --wait \ 98 | --set installCRDs=true \ 99 | --set extraArgs='{--enable-certificate-owner-ref}' 100 | 101 | - name: Prepare existing secret test scenario 102 | if: steps.changes.outputs.changed == 'true' 103 | run: | 104 | openssl req -x509 -newkey rsa:2048 -sha256 -days 365 \ 105 | -nodes -keyout ${{ runner.temp }}/tls.key -out ${{ runner.temp }}/tls.crt \ 106 | -subj "/CN=metrics-server" \ 107 | -addext "subjectAltName=DNS:metrics-server,DNS:metrics-server.kube-system.svc" 108 | 109 | kubectl -n kube-system create secret generic metrics-server-existing \ 110 | --from-file=${{ runner.temp }}/tls.key \ 111 | --from-file=${{ runner.temp }}/tls.crt 112 | 113 | cat <> charts/metrics-server/ci/tls-existingSecret-values.yaml 114 | apiService: 115 | insecureSkipTLSVerify: false 116 | caBundle: | 117 | $(cat ${{ runner.temp }}/tls.crt | sed -e "s/^/ /g") 118 | EOF 119 | 120 | rm ${{ runner.temp }}/tls.key ${{ runner.temp }}/tls.crt 121 | 122 | - name: Run chart-testing install 123 | if: steps.changes.outputs.changed == 'true' 124 | run: ct install --namespace kube-system 125 | -------------------------------------------------------------------------------- /.github/workflows/release-chart.yaml: -------------------------------------------------------------------------------- 1 | name: Release Chart 2 | 3 | on: 4 | push: 5 | branches: 6 | - release-* 7 | paths: 8 | - charts/metrics-server/Chart.yaml 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | release: 15 | name: Release 16 | if: github.repository == 'kubernetes-sigs/metrics-server' 17 | runs-on: ubuntu-latest 18 | defaults: 19 | run: 20 | shell: bash 21 | permissions: 22 | contents: write 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 26 | with: 27 | fetch-depth: 0 28 | 29 | - name: Configure Git 30 | run: | 31 | git config user.name "$GITHUB_ACTOR" 32 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 33 | 34 | - name: Get chart version 35 | id: chart_version 36 | uses: mikefarah/yq@bbdd97482f2d439126582a59689eb1c855944955 # v4.44.3 37 | with: 38 | cmd: yq eval '.version' './charts/metrics-server/Chart.yaml' 39 | 40 | - name: Get chart app version 41 | id: chart_app_version 42 | uses: mikefarah/yq@bbdd97482f2d439126582a59689eb1c855944955 # v4.44.3 43 | with: 44 | cmd: yq eval '.appVersion' './charts/metrics-server/Chart.yaml' 45 | 46 | - name: Check can release 47 | id: check_can_release 48 | run: | 49 | set -euo pipefail 50 | 51 | branch_name="${GITHUB_REF##*/}" 52 | app_version_prefix="${branch_name##*-}" 53 | 54 | app_version_regex="^${app_version_prefix//./\.}" 55 | 56 | chart_version_match="$(echo "${{ steps.chart_version.outputs.result }}" | grep -Po "^\d+\.\d+\.\d+$" || true)" 57 | app_version_match="$(echo "${{ steps.chart_app_version.outputs.result }}" | grep -Po "^${app_version_prefix//./\.}" || true)" 58 | 59 | if [[ -z "${chart_version_match}" ]] || [[ -z "${app_version_match}" ]] 60 | then 61 | echo "continue=false" >> $GITHUB_OUTPUT 62 | else 63 | echo "continue=true" >> $GITHUB_OUTPUT 64 | fi 65 | 66 | - name: Set-up Helm 67 | if: steps.check_can_release.outputs.continue == 'true' 68 | uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0 69 | with: 70 | token: ${{ secrets.GITHUB_TOKEN }} 71 | version: latest 72 | 73 | - name: Get CHANGELOG entry 74 | id: changelog_reader 75 | if: steps.check_can_release.outputs.continue == 'true' 76 | uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2.2.3 77 | with: 78 | path: charts/metrics-server/CHANGELOG.md 79 | version: ${{ steps.chart_version.outputs.result }} 80 | 81 | - name: Create release notes 82 | if: steps.check_can_release.outputs.continue == 'true' 83 | run: | 84 | set -euo pipefail 85 | cat <<"EOF" > charts/metrics-server/RELEASE.md 86 | ${{ steps.changelog_reader.outputs.changes }} 87 | EOF 88 | 89 | - name: Run chart-releaser 90 | if: steps.check_can_release.outputs.continue == 'true' 91 | uses: helm/chart-releaser-action@a917fd15b20e8b64b94d9158ad54cd6345335584 # v1.6.0 92 | env: 93 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 94 | CR_RELEASE_NAME_TEMPLATE: "metrics-server-helm-chart-{{ .Version }}" 95 | CR_RELEASE_NOTES_FILE: RELEASE.md 96 | CR_MAKE_RELEASE_LATEST: false 97 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build: 13 | name: build 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | shell: bash 18 | permissions: 19 | contents: write 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 23 | - name: Build manifests 24 | run: make release-manifests 25 | - name: Release manifests 26 | uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v0.1.15 27 | with: 28 | files: | 29 | _output/components.yaml 30 | _output/high-availability.yaml 31 | _output/high-availability-1.21+.yaml 32 | - name: Build binaries 33 | run: make build-all 34 | - name: Release binaries 35 | uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v0.1.15 36 | with: 37 | files: | 38 | _output/metrics-server-* 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_output 2 | /metrics-server 3 | .cover 4 | 5 | # Vim-related files 6 | [._]*.s[a-w][a-z] 7 | [._]s[a-w][a-z] 8 | *~ 9 | *.un~ 10 | Session.vim 11 | .netrwhist 12 | .idea 13 | *manifest-tool 14 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 10m 3 | 4 | linters: 5 | disable-all: true 6 | enable: 7 | - bodyclose 8 | - dogsled 9 | - dupl 10 | - errcheck 11 | - copyloopvar 12 | - exhaustive 13 | - gocritic 14 | - gocyclo 15 | - gofmt 16 | - goimports 17 | - goprintffuncname 18 | - gosimple 19 | - govet 20 | - ineffassign 21 | - misspell 22 | - nakedret 23 | - nolintlint 24 | - staticcheck 25 | - unconvert 26 | - unused 27 | - whitespace 28 | 29 | linters-settings: 30 | goimports: 31 | local-prefixes: sigs.k8s.io/metrics-server 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Welcome to Kubernetes. We are excited about the prospect of you joining our [community](https://git.k8s.io/community)! The Kubernetes community abides by the CNCF [code of conduct](code-of-conduct.md). Here is an excerpt: 4 | 5 | _As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities._ 6 | 7 | ## Getting Started 8 | 9 | We have full documentation on how to get started contributing here: 10 | 11 | 14 | 15 | - [Contributor License Agreement](https://git.k8s.io/community/CLA.md) Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests 16 | - [Kubernetes Contributor Guide](https://git.k8s.io/community/contributors/guide) - Main contributor documentation, or you can just jump directly to the [contributing section](https://git.k8s.io/community/contributors/guide#contributing) 17 | - [Contributor Cheat Sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet) - Common resources for existing developers 18 | 19 | ## Mentorship 20 | 21 | - [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers! 22 | 23 | ## Chart Changes 24 | 25 | When contributing chart changes please follow the same process as when contributing other content but also please **DON'T** modify _Chart.yaml_ in the PR as this would result in a chart release when merged and will mean that your PR will need modifying before it can be accepted. The chart version will be updated as part of the PR to release the chart. 26 | 27 | ## Development 28 | 29 | Required tools: 30 | 31 | - [Docker](https://www.docker.com/) 32 | - [Kind](https://kind.sigs.k8s.io/) 33 | - [Skaffold](https://skaffold.dev/) 34 | 35 | ## Adding dependencies 36 | 37 | The project follows a standard Go project layout, see more about [dependency-management](https://github.com/kubernetes/community/blob/master/contributors/devel/development.md#dependency-management). 38 | 39 | ## Running static code validation 40 | 41 | ```sh 42 | make lint 43 | ``` 44 | 45 | ## Running tests 46 | 47 | ```sh 48 | make test-unit 49 | make test-version 50 | make test-e2e 51 | ``` 52 | 53 | ## Live reload 54 | 55 | To start local development just run: 56 | 57 | ```sh 58 | kind create cluster 59 | skaffold dev 60 | ``` 61 | 62 | To execute e2e tests run: 63 | 64 | ```sh 65 | go test test/e2e_test.go -v -count=1 66 | ``` 67 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Update the base image in Makefile when updating golang version. This has to 2 | # be pre-pulled in order to work on GCB. 3 | ARG ARCH 4 | FROM golang:1.24.2 as build 5 | 6 | WORKDIR /go/src/sigs.k8s.io/metrics-server 7 | COPY go.mod . 8 | COPY go.sum . 9 | RUN go mod download 10 | 11 | COPY pkg pkg 12 | COPY cmd cmd 13 | COPY Makefile Makefile 14 | 15 | ARG ARCH 16 | ARG GIT_COMMIT 17 | ARG GIT_TAG 18 | RUN make metrics-server 19 | 20 | FROM gcr.io/distroless/static:latest-$ARCH 21 | COPY --from=build /go/src/sigs.k8s.io/metrics-server/metrics-server / 22 | USER 65534 23 | ENTRYPOINT ["/metrics-server"] 24 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## Table of Contents 4 | 5 | 6 | - [What metrics are exposed by the metrics server?](#what-metrics-are-exposed-by-the-metrics-server) 7 | - [How CPU usage is calculated?](#how-cpu-usage-is-calculated) 8 | - [How memory usage is calculated?](#how-memory-usage-is-calculated) 9 | - [How does the metrics server calculate metrics?](#how-does-the-metrics-server-calculate-metrics) 10 | - [How often is metrics server released?](#how-often-is-metrics-server-released) 11 | - [Can I run more than one instance of metrics-server?](#can-i-run-more-than-one-instance-of-metrics-server) 12 | - [How to run metrics-server securely?](#how-to-run-metrics-server-securely) 13 | - [How to run metric-server on different architecture?](#how-to-run-metric-server-on-different-architecture) 14 | - [What Kubernetes versions are supported?](#what-kubernetes-versions-are-supported) 15 | - [How is resource utilization calculated?](#how-is-resource-utilization-calculated) 16 | - [How to autoscale Metrics Server?](#how-to-autoscale-metrics-server) 17 | - [Can I get other metrics beside CPU/Memory using Metrics Server?](#can-i-get-other-metrics-beside-cpumemory-using-metrics-server) 18 | - [How large can clusters be?](#how-large-can-clusters-be) 19 | - [How often metrics are scraped?](#how-often-metrics-are-scraped) 20 | 21 | 22 | ### What metrics are exposed by the metrics server? 23 | 24 | Metrics server collects resource usage metrics needed for autoscaling: CPU & Memory. 25 | Metrics values use [Metric System prefixes] (`n` = 10-9 and `Ki` = 210), 26 | the same as those used to define pod requests and limits. 27 | Metrics server itself is not responsible for calculating metric values, this is done by Kubelet. 28 | 29 | [Metric System prefixes]: https://en.wikipedia.org/wiki/Metric_prefix 30 | 31 | ### How CPU usage is calculated? 32 | 33 | CPU is reported as the average core usage measured in cpu units. 34 | One cpu, in Kubernetes, is equivalent to 1 vCPU/Core for cloud providers and 1 hyperthread on bare-metal Intel processors. 35 | 36 | This value is derived by taking a rate over a cumulative CPU counter provided by the kernel (in both Linux and Windows kernels). 37 | Time window used to calculate CPU is exposed under `window` field in Metrics API. 38 | 39 | Read more about [Meaning of CPU]. 40 | 41 | [Meaning of CPU]: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu 42 | 43 | ### How memory usage is calculated? 44 | 45 | Memory is reported as the working set at the instant the metric was collected, measured in bytes. 46 | 47 | In an ideal world, the "working set" is the amount of memory in-use that cannot be freed under memory pressure. 48 | However, calculation of the working set varies by host OS, and generally makes heavy use of heuristics to produce an estimate. 49 | It includes all anonymous (non-file-backed) memory since Kubernetes does not support swap. 50 | The metric typically also includes some cached (file-backed) memory, because the host OS cannot always reclaim such pages. 51 | 52 | Read more about [Meaning of memory]. 53 | 54 | [Meaning of memory]: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory 55 | 56 | ### How does the metrics server calculate metrics? 57 | 58 | Metrics Server itself doesn't calculate any metrics, it aggregates values exposed by Kubelet and exposes them in API 59 | to be used for autoscaling. For any problem with metric values please contact SIG-Node. 60 | 61 | ### How often is metrics server released? 62 | 63 | There is no hard release schedule. A release is done after an important feature is implemented or upon request. 64 | 65 | ### Can I run more than one instance of metrics-server? 66 | 67 | Yes, more than one instance can be deployed for High Availability. Each instance of the metrics server will scrape all nodes to collect metrics, but only one instance will be actively serving metrics API. it is **recommended** to add the `--enable-aggregator-routing=true` flag to the `kube-apiserver` so that requests sent to the Metrics Server are load balanced. [More Info.](./README.md?#high-availability) 68 | 69 | ### How to run metrics-server securely? 70 | 71 | Suggested configuration: 72 | 73 | - Cluster with [RBAC] enabled 74 | - Kubelet [read-only port] port disabled 75 | - Validate kubelet certificate by mounting CA file and providing `--kubelet-certificate-authority` flag to metrics server 76 | - Avoid passing insecure flags to metrics server (`--deprecated-kubelet-completely-insecure`, `--kubelet-insecure-tls`) 77 | - Consider using your own certificates (`--tls-cert-file`, `--tls-private-key-file`) 78 | 79 | ### How to run metric-server on different architecture? 80 | 81 | Starting from `v0.3.7` docker image `registry.k8s.io/metrics-server/metrics-server` should support multiple architectures via Manifests List. 82 | List of supported architectures: `amd64`, `arm`, `arm64`, `ppc64le`, `s390x`. 83 | 84 | ### What Kubernetes versions are supported? 85 | 86 | Metrics server is tested against the last 3 Kubernetes versions. 87 | 88 | ### How is resource utilization calculated? 89 | 90 | Metrics server doesn't provide resource utilization metrics (e.g. percent of CPU used). 91 | Utilization presented by `kubectl top` and HPA is calculated client side based on pod resource requests or node capacity. 92 | 93 | ### How to autoscale Metrics Server? 94 | 95 | Metrics server scales linearly vertically according to the number of nodes and pods in a cluster. This can be automated using [addon-resizer]. 96 | 97 | ### Can I get other metrics beside CPU/Memory using Metrics Server? 98 | 99 | No, metrics server was designed to provide metrics for [resource metrics pipeline] used for autoscaling. 100 | 101 | ### How large can clusters be? 102 | 103 | Metrics Server was tested to run within clusters up to 5000 nodes with an average pod density of 30 pods per node. 104 | 105 | ### How often metrics are scraped? 106 | 107 | Default 60 seconds, can be changed using `metric-resolution` flag. We are not recommending setting values below 15s, as this is the resolution of metrics calculated by Kubelet. 108 | 109 | [RBAC]: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ 110 | [read-only port]: https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/#options 111 | [addon-resizer]: https://github.com/kubernetes/autoscaler/tree/master/addon-resizer 112 | [resource metrics pipeline]: https://kubernetes.io/docs/tasks/debug-application-cluster/resource-metrics-pipeline/ 113 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs: https://go.k8s.io/owners 2 | approvers: 3 | - s-urbaniak 4 | - serathius 5 | - dgrisonnet 6 | - logicalhan 7 | reviewers: 8 | - dgrisonnet 9 | - s-urbaniak 10 | - serathius 11 | - yangjunmyfm192085 12 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | The Metrics Server is released on an as-needed basis. The process is as follows: 4 | 5 | 1. An issue is proposing a new release with a changelog since the last release https://github.com/kubernetes-sigs/metrics-server/compare 6 | 1. At least one [OWNER](OWNERS) must LGTM this release 7 | 1. A PR that bumps version hardcoded in code is created and merged 8 | 1. An OWNER creates a draft GitHub release 9 | 1. An OWNER creates an issue to release the corresponding Helm chart via the chart release process (below) 10 | 1. An OWNER creates a release tag using `GIT_TAG=$VERSION make release-tag` and waits for [prow.k8s.io](prow.k8s.io) to build and push new images to [gcr.io/k8s-staging-metrics-server](https://gcr.io/k8s-staging-metrics-server) 11 | 1. A PR in [kubernetes/k8s.io](https://github.com/kubernetes/k8s.io/blob/main/k8s.gcr.io/images/k8s-staging-metrics-server/images.yaml) is created to release images to `k8s.gcr.io` 12 | 1. An OWNER publishes the GitHub release. Once published, release manifests will be automatically added to the release by CI. 13 | 1. An announcement email is sent to `kubernetes-sig-instrumentation@googlegroups.com` with the subject `[ANNOUNCE] metrics-server $VERSION is released` 14 | 1. The release issue is closed 15 | 16 | ## Chart Release Process 17 | 18 | The chart needs to be released in response to a Metrics Server release or on an as-needed basis. The process is as follows: 19 | 20 | 1. An issue is proposing a new chart release 21 | 1. A PR is opened to update _Chart.yaml_ with the `appVersion`, `version` (based on the release changes) and `annotations` 22 | 1. The PR triggers the chart linting and testing GitHub action to validate the chart 23 | 1. The PR is merged and a GitHub action releases the chart 24 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Security Announcements 4 | 5 | Join the [kubernetes-security-announce] group for security and vulnerability announcements. 6 | 7 | You can also subscribe to an RSS feed of the above using [this link][kubernetes-security-announce-rss]. 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Instructions for reporting a vulnerability can be found on the 12 | [Kubernetes Security and Disclosure Information] page. 13 | 14 | ## Supported Versions 15 | 16 | Information about supported Kubernetes versions can be found on the 17 | [Kubernetes version and version skew support policy] page on the Kubernetes website. 18 | 19 | [kubernetes-security-announce]: https://groups.google.com/forum/#!forum/kubernetes-security-announce 20 | [kubernetes-security-announce-rss]: https://groups.google.com/forum/feed/kubernetes-security-announce/msgs/rss_v2_0.xml?num=50 21 | [Kubernetes version and version skew support policy]: https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions 22 | [Kubernetes Security and Disclosure Information]: https://kubernetes.io/docs/reference/issues-security/security/#report-a-vulnerability 23 | -------------------------------------------------------------------------------- /SECURITY_CONTACTS: -------------------------------------------------------------------------------- 1 | # Defined below are the security contacts for this repo. 2 | # 3 | # They are the contact point for the Product Security Committee to reach out 4 | # to for triaging and handling of incoming issues. 5 | # 6 | # The below names agree to abide by the 7 | # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) 8 | # and will be removed and replaced if they violate that agreement. 9 | # 10 | # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE 11 | # INSTRUCTIONS AT https://kubernetes.io/security/ 12 | 13 | s-urbaniak 14 | serathius 15 | -------------------------------------------------------------------------------- /charts/OWNERS: -------------------------------------------------------------------------------- 1 | labels: 2 | - chart 3 | approvers: 4 | - stevehipwell 5 | reviewers: 6 | - stevehipwell 7 | -------------------------------------------------------------------------------- /charts/metrics-server/.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 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/metrics-server/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Metrics Server Helm Chart Changelog 2 | 3 | > [!NOTE] 4 | > All notable changes to this project will be documented in this file; the format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 5 | 6 | 14 | 15 | ## [UNRELEASED] 16 | 17 | ### Added 18 | 19 | - Add options on howto secure the connection between metrics-server and the kube-apiserver. ([#1288](https://github.com/kubernetes-sigs/metrics-server/pull/1288)) _@mkilchhofer_ 20 | - Added `unhealthyPodEvictionPolicy` to the metrics-server PDB as a user enabled feature. ([#1574](https://github.com/kubernetes-sigs/metrics-server/pull/1574)) @peterabarr 21 | 22 | ### Changed 23 | 24 | - Updated the _Addon Resizer_ OCI image to [`1.8.23`](https://github.com/kubernetes/autoscaler/releases/tag/addon-resizer-1.8.23). ([#1626](https://github.com/kubernetes-sigs/metrics-server/pull/1626)) _@stevehipwell_ 25 | 26 | ## [3.12.2] - TBC 27 | 28 | ### Added 29 | 30 | - Explicitly added the app protocol to the service. ([#1540](https://github.com/kubernetes-sigs/metrics-server/pull/1540)) _@ 31 | seankhliao_ 32 | 33 | ### Changed 34 | 35 | - Updated the _Metrics Server_ OCI image to [v0.7.2](https://github.com/kubernetes-sigs/metrics-server/releases/tag/v0.7.2). ([#1568](https://github.com/kubernetes-sigs/metrics-server/pull/1568)) _@stevehipwell_ 36 | - Updated the _addonResizer_ OCI image to [1.8.21](https://github.com/kubernetes/autoscaler/releases/tag/addon-resizer-1.8.21). ([#1504](https://github.com/kubernetes-sigs/metrics-server/pull/1504)) _@jimmy-ungerman_ 37 | 38 | ### Fixed 39 | 40 | - Fixed nanny's RoleBinding which contained a hard-coded namespace instead of the Helm's release namespace. ([#1479](https://github.com/kubernetes-sigs/metrics-server/pull/1479)) _@the-technat_ 41 | - Fixed the `ServiceMonitor` job label. ([#1568](https://github.com/kubernetes-sigs/metrics-server/pull/1568)) _@stevehipwell_ 42 | 43 | ## [3.12.1] - 2024-04-05 44 | 45 | ### Changed 46 | 47 | - Updated the _Metrics Server_ OCI image to [v0.7.1](https://github.com/kubernetes-sigs/metrics-server/releases/tag/v0.7.1). ([#1461](https://github.com/kubernetes-sigs/metrics-server/pull/1461)) _@stevehipwell_ 48 | - Changed `Deployment` templating to ignore `schedulerName` when value is empty. ([#1475](https://github.com/kubernetes-sigs/metrics-server/pull/1475)) _@senges_ 49 | 50 | ## [3.12.0] - 2024-02-07 51 | 52 | ### Changed 53 | 54 | - Updated the _Metrics Server_ OCI image to [v0.7.0](https://github.com/kubernetes-sigs/metrics-server/releases/tag/v0.7.0). ([#1414](https://github.com/kubernetes-sigs/metrics-server/pull/1414)) [@stevehipwell](https://github.com/stevehipwell) 55 | - Updated the _addon-resizer_ OCI image to [v1.8.20](https://github.com/kubernetes/autoscaler/releases/tag/addon-resizer-1.8.20). ([#1414](https://github.com/kubernetes-sigs/metrics-server/pull/1414)) [@stevehipwell](https://github.com/stevehipwell) 56 | 57 | ## [3.11.0] - 2023-08-03 58 | 59 | ### Added 60 | 61 | - Added default _Metrics Server_ resource requests. 62 | 63 | ### Changed 64 | 65 | - Updated the _Metrics Server_ OCI image to [v0.6.4](https://github.com/kubernetes-sigs/metrics-server/releases/tag/v0.6.4). 66 | - Updated the _addon-resizer_ OCI image to [v1.8.19](https://github.com/kubernetes/autoscaler/releases/tag/addon-resizer-1.8.19). 67 | 68 | ## [3.10.0] - 2023-04-12 69 | 70 | ### Added 71 | 72 | - Added support for running under PodSecurity restricted. 73 | 74 | ### Fixed 75 | 76 | - Fixed `auth-reader` role binding namespace to always use `kube-system`. 77 | - Fixed addon-resizer configuration. 78 | - Fixed container port default not having been updated to `10250`. 79 | 80 | ## [3.9.0] - 2023-03-28 81 | 82 | ### Added 83 | 84 | - Added autoscaling support via the addon-resizer. 85 | 86 | ### Changed 87 | 88 | - Updated the _Metrics Server_ OCI image to [v0.6.3](https://github.com/kubernetes-sigs/metrics-server/releases/tag/v0.6.3). 89 | 90 | ### Fixed 91 | 92 | - Fixed service labels/annotations. 93 | 94 | ## [3.8.4] - 2023-03-06 95 | 96 | ### Changed 97 | 98 | - Changed the image registry location to `registry.k8s.io`. 99 | 100 | ## [3.8.3] - 2022-12-08 101 | 102 | ### Added 103 | 104 | - Added support for topologySpreadConstraints. 105 | - Always set resource namespaces explicitly. 106 | - Allow configuring TLS on the APIService. 107 | - Enabled service monitor relabelling. 108 | - Added ability to set the scheduler name. 109 | - Added support for common labels. 110 | 111 | ### Changed 112 | 113 | - Updated the _Metrics Server_ OCI image to [v0.6.2](https://github.com/kubernetes-sigs/metrics-server/releases/tag/v0.6.2). 114 | 115 | ## [3.8.2] - 2022-02-23 116 | 117 | ### Changed 118 | 119 | - Changed chart to allow probes to be turned off completely (this is not advised unless you know what you're doing). 120 | 121 | ## [3.8.1] - 2022-02-09 122 | 123 | ### Changed 124 | 125 | - Updated the _Metrics Server_ OCI image to [v0.6.1](https://github.com/kubernetes-sigs/metrics-server/releases/tag/v0.6.1). 126 | 127 | ## [3.8.0] - 2022-02-08 128 | 129 | ### Added 130 | 131 | - Added support for unauthenticated access to the /metrics endpoint. 132 | - Added optional _Prometheus Operator_ `ServiceMonitor`. 133 | 134 | ### Changed 135 | 136 | - Updated the _Metrics Server_ OCI image to [v0.6.0](https://github.com/kubernetes-sigs/metrics-server/releases/tag/v0.6.0). 137 | 138 | ## [3.7.0] - 2021-11-18 139 | 140 | ### Changed 141 | 142 | - Updated the _Metrics Server_ OCI image to [v0.5.2](https://github.com/kubernetes-sigs/metrics-server/releases/tag/v0.5.2). 143 | 144 | ## [3.6.0] - 2021-10-18 145 | 146 | ### Added 147 | 148 | - Added new `defaultArgs`` value to enable overriding the default arguments. 149 | 150 | ### Changed 151 | 152 | - Updated the _Metrics Server_ OCI image to [v0.5.1](https://github.com/kubernetes-sigs/metrics-server/releases/tag/v0.5.1). 153 | 154 | ## [3.5.0] - 2021-10-07 155 | 156 | ### Added 157 | 158 | - Added initial Helm chart release from official repo. 159 | 160 | 163 | [UNRELEASED]: https://github.com/kubernetes-sigs/metrics-server/tree/master/charts/metrics-server 164 | [3.12.2]: https://github.com/kubernetes-sigs/metrics-server/releases/tag/metrics-server-helm-chart-3.12.2 165 | [3.12.1]: https://github.com/kubernetes-sigs/metrics-server/releases/tag/metrics-server-helm-chart-3.12.1 166 | [3.12.0]: https://github.com/kubernetes-sigs/metrics-server/releases/tag/metrics-server-helm-chart-3.12.0 167 | [3.11.0]: https://github.com/kubernetes-sigs/metrics-server/releases/tag/metrics-server-helm-chart-3.11.0 168 | [3.10.0]: https://github.com/kubernetes-sigs/metrics-server/releases/tag/metrics-server-helm-chart-3.10.0 169 | [3.9.0]: https://github.com/kubernetes-sigs/metrics-server/releases/tag/metrics-server-helm-chart-3.9.0 170 | [3.8.4]: https://github.com/kubernetes-sigs/metrics-server/releases/tag/metrics-server-helm-chart-3.8.4 171 | [3.8.3]: https://github.com/kubernetes-sigs/metrics-server/releases/tag/metrics-server-helm-chart-3.8.3 172 | [3.8.2]: https://github.com/kubernetes-sigs/metrics-server/releases/tag/metrics-server-helm-chart-3.8.2 173 | [3.8.1]: https://github.com/kubernetes-sigs/metrics-server/releases/tag/metrics-server-helm-chart-3.8.1 174 | [3.8.0]: https://github.com/kubernetes-sigs/metrics-server/releases/tag/metrics-server-helm-chart-3.8.0 175 | [3.7.0]: https://github.com/kubernetes-sigs/metrics-server/releases/tag/metrics-server-helm-chart-3.7.0 176 | [3.6.0]: https://github.com/kubernetes-sigs/metrics-server/releases/tag/metrics-server-helm-chart-3.6.0 177 | [3.5.0]: https://github.com/kubernetes-sigs/metrics-server/releases/tag/metrics-server-helm-chart-3.5.0 178 | -------------------------------------------------------------------------------- /charts/metrics-server/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: metrics-server 3 | description: Metrics Server is a scalable, efficient source of container resource metrics for Kubernetes built-in autoscaling pipelines. 4 | type: application 5 | version: 3.12.2 6 | appVersion: 0.7.2 7 | keywords: 8 | - kubernetes 9 | - metrics-server 10 | - metrics 11 | home: https://github.com/kubernetes-sigs/metrics-server 12 | icon: https://avatars.githubusercontent.com/u/36015203?s=400&v=4 13 | sources: 14 | - https://github.com/kubernetes-sigs/metrics-server 15 | maintainers: 16 | - name: stevehipwell 17 | url: https://github.com/stevehipwell 18 | - name: krmichel 19 | url: https://github.com/krmichel 20 | - name: endrec 21 | url: https://github.com/endrec 22 | annotations: 23 | artifacthub.io/changes: | 24 | - kind: added 25 | description: "Explicitly added the app protocol to the service." 26 | - kind: changed 27 | description: "Updated the _Metrics Server_ OCI image to [v0.7.2](https://github.com/kubernetes-sigs/metrics-server/releases/tag/v0.7.2)." 28 | - kind: changed 29 | description: "Updated the _addonResizer_ OCI image to [1.8.21](https://github.com/kubernetes/autoscaler/releases/tag/addon-resizer-1.8.21)" 30 | - kind: fixed 31 | description: "Fixed nanny's RoleBinding which contained a hard-coded namespace instead of the Helm's release namespace." 32 | -------------------------------------------------------------------------------- /charts/metrics-server/ci/ci-values.yaml: -------------------------------------------------------------------------------- 1 | args: 2 | - --kubelet-insecure-tls 3 | -------------------------------------------------------------------------------- /charts/metrics-server/ci/tls-certManager-values.yaml: -------------------------------------------------------------------------------- 1 | args: 2 | - --kubelet-insecure-tls 3 | 4 | apiService: 5 | insecureSkipTLSVerify: false 6 | 7 | tls: 8 | type: cert-manager 9 | -------------------------------------------------------------------------------- /charts/metrics-server/ci/tls-existingSecret-values.yaml: -------------------------------------------------------------------------------- 1 | args: 2 | - --kubelet-insecure-tls 3 | 4 | ## Set via GH action (step "Prepare existing secret test scenario") 5 | # apiService: 6 | # insecureSkipTLSVerify: false 7 | # caBundle: | 8 | 9 | tls: 10 | type: existingSecret 11 | existingSecret: 12 | name: metrics-server-existing 13 | -------------------------------------------------------------------------------- /charts/metrics-server/ci/tls-helm-values.yaml: -------------------------------------------------------------------------------- 1 | args: 2 | - --kubelet-insecure-tls 3 | 4 | apiService: 5 | insecureSkipTLSVerify: false 6 | 7 | tls: 8 | type: helm 9 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | *********************************************************************** 2 | * Metrics Server * 3 | *********************************************************************** 4 | Chart version: {{ .Chart.Version }} 5 | App version: {{ .Chart.AppVersion }} 6 | Image tag: {{ include "metrics-server.image" . }} 7 | *********************************************************************** 8 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "metrics-server.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "metrics-server.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "metrics-server.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "metrics-server.labels" -}} 37 | helm.sh/chart: {{ include "metrics-server.chart" . }} 38 | {{ include "metrics-server.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- if .Values.commonLabels }} 44 | {{ toYaml .Values.commonLabels }} 45 | {{- end }} 46 | {{- end }} 47 | 48 | {{/* 49 | Selector labels 50 | */}} 51 | {{- define "metrics-server.selectorLabels" -}} 52 | app.kubernetes.io/name: {{ include "metrics-server.name" . }} 53 | app.kubernetes.io/instance: {{ .Release.Name }} 54 | {{- end }} 55 | 56 | {{/* 57 | Create the name of the service account to use 58 | */}} 59 | {{- define "metrics-server.serviceAccountName" -}} 60 | {{- if .Values.serviceAccount.create }} 61 | {{- default (include "metrics-server.fullname" .) .Values.serviceAccount.name }} 62 | {{- else }} 63 | {{- default "default" .Values.serviceAccount.name }} 64 | {{- end }} 65 | {{- end }} 66 | 67 | {{/* 68 | The image to use 69 | */}} 70 | {{- define "metrics-server.image" -}} 71 | {{- printf "%s:%s" .Values.image.repository (default (printf "v%s" .Chart.AppVersion) .Values.image.tag) }} 72 | {{- end }} 73 | 74 | {{/* 75 | The image to use for the addon resizer 76 | */}} 77 | {{- define "metrics-server.addonResizer.image" -}} 78 | {{- printf "%s:%s" .Values.addonResizer.image.repository .Values.addonResizer.image.tag }} 79 | {{- end }} 80 | 81 | {{/* 82 | ConfigMap name of addon resizer 83 | */}} 84 | {{- define "metrics-server.addonResizer.configMap" -}} 85 | {{- printf "%s-%s" (include "metrics-server.fullname" .) "nanny-config" }} 86 | {{- end }} 87 | 88 | {{/* 89 | Role name of addon resizer 90 | */}} 91 | {{- define "metrics-server.addonResizer.role" -}} 92 | {{ printf "system:%s-nanny" (include "metrics-server.fullname" .) }} 93 | {{- end }} 94 | 95 | {{/* Get PodDisruptionBudget API Version */}} 96 | {{- define "metrics-server.pdb.apiVersion" -}} 97 | {{- if and (.Capabilities.APIVersions.Has "policy/v1") (semverCompare ">= 1.21-0" .Capabilities.KubeVersion.Version) -}} 98 | {{- print "policy/v1" -}} 99 | {{- else -}} 100 | {{- print "policy/v1beta1" -}} 101 | {{- end -}} 102 | {{- end -}} 103 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/apiservice.yaml: -------------------------------------------------------------------------------- 1 | {{- $altNames := list }} 2 | {{- $certs := dict }} 3 | {{- $previous := dict }} 4 | 5 | {{- if eq .Values.tls.type "helm" }} 6 | {{- $previous = lookup "v1" "Secret" .Release.Namespace (include "metrics-server.fullname" .) }} 7 | {{- $commonName := include "metrics-server.fullname" . }} 8 | {{- $ns := .Release.Namespace }} 9 | {{- $altNames = append $altNames (printf "%s.%s" $commonName $ns) }} 10 | {{- $altNames = append $altNames (printf "%s.%s.svc" $commonName $ns) }} 11 | {{- $altNames = append $altNames (printf "%s.%s.svc.%s" $commonName $ns .Values.tls.clusterDomain) }} 12 | {{- $certs = genSelfSignedCert $commonName nil $altNames (int .Values.tls.helm.certDurationDays) }} 13 | apiVersion: v1 14 | kind: Secret 15 | metadata: 16 | name: {{ include "metrics-server.fullname" . }} 17 | labels: 18 | {{- include "metrics-server.labels" . | nindent 4 }} 19 | type: Opaque 20 | data: 21 | {{- if and $previous .Values.tls.helm.lookup }} 22 | tls.crt: {{ index $previous.data "tls.crt" }} 23 | tls.key: {{ index $previous.data "tls.key" }} 24 | {{- else }} 25 | tls.crt: {{ $certs.Cert| b64enc | quote }} 26 | tls.key: {{ $certs.Key | b64enc | quote }} 27 | {{- end }} 28 | {{- end }} 29 | --- 30 | {{- $existing := dict }} 31 | {{- if .Values.apiService.create }} 32 | {{- if and (eq .Values.tls.type "existingSecret") .Values.tls.existingSecret.lookup }} 33 | {{- $existing := lookup "v1" "Secret" .Release.Namespace .Values.tls.existingSecret.name }} 34 | {{- end }} 35 | apiVersion: apiregistration.k8s.io/v1 36 | kind: APIService 37 | metadata: 38 | name: v1beta1.metrics.k8s.io 39 | labels: 40 | {{- include "metrics-server.labels" . | nindent 4 }} 41 | {{- if or .Values.apiService.annotations .Values.tls.certManager.addInjectorAnnotations }} 42 | annotations: 43 | {{- if and (eq .Values.tls.type "cert-manager") .Values.tls.certManager.addInjectorAnnotations }} 44 | cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "metrics-server.fullname" . }} 45 | {{- end }} 46 | {{- with .Values.apiService.annotations }} 47 | {{- toYaml . | nindent 4 }} 48 | {{- end }} 49 | {{- end }} 50 | spec: 51 | {{- if eq .Values.tls.type "helm" }} 52 | {{- if and $previous .Values.tls.helm.lookup }} 53 | caBundle: {{ index $previous.data "tls.crt" }} 54 | {{- else }} 55 | caBundle: {{ $certs.Cert | b64enc }} 56 | {{- end }} 57 | {{- else if $existing }} 58 | caBundle: {{ index $existing.data "tls.crt" }} 59 | {{- else if and .Values.apiService.caBundle (ne .Values.tls.type "cert-manager") }} 60 | caBundle: {{ .Values.apiService.caBundle | b64enc }} 61 | {{- end }} 62 | group: metrics.k8s.io 63 | groupPriorityMinimum: 100 64 | insecureSkipTLSVerify: {{ .Values.apiService.insecureSkipTLSVerify }} 65 | service: 66 | name: {{ include "metrics-server.fullname" . }} 67 | namespace: {{ .Release.Namespace }} 68 | port: {{ .Values.service.port }} 69 | version: v1beta1 70 | versionPriority: 100 71 | {{- end }} 72 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/certificate.yaml: -------------------------------------------------------------------------------- 1 | {{- if eq .Values.tls.type "cert-manager" }} 2 | {{- if not .Values.tls.certManager.existingIssuer.enabled }} 3 | apiVersion: cert-manager.io/v1 4 | kind: Issuer 5 | metadata: 6 | annotations: 7 | {{- toYaml .Values.additionalAnnotations | nindent 4 }} 8 | name: {{ include "metrics-server.fullname" . }}-issuer 9 | namespace: {{ .Release.Namespace }} 10 | spec: 11 | selfSigned: {} 12 | {{- end }} 13 | --- 14 | apiVersion: cert-manager.io/v1 15 | kind: Certificate 16 | metadata: 17 | name: {{ include "metrics-server.fullname" . }} 18 | namespace: {{ .Release.Namespace }} 19 | spec: 20 | commonName: {{ include "metrics-server.fullname" . }} 21 | dnsNames: 22 | - {{ include "metrics-server.fullname" . }}.{{ .Release.Namespace }} 23 | - {{ include "metrics-server.fullname" . }}.{{ .Release.Namespace }}.svc 24 | - {{ include "metrics-server.fullname" . }}.{{ .Release.Namespace }}.svc.{{ .Values.tls.clusterDomain }} 25 | secretName: {{ include "metrics-server.fullname" . }} 26 | usages: 27 | - server auth 28 | - client auth 29 | privateKey: 30 | algorithm: RSA 31 | size: 2048 32 | {{- with .Values.tls.certManager.duration }} 33 | duration: {{ . }} 34 | {{- end }} 35 | {{- with .Values.tls.certManager.renewBefore }} 36 | renewBefore: {{ . }} 37 | {{- end }} 38 | issuerRef: 39 | {{- if .Values.tls.certManager.existingIssuer.enabled }} 40 | name: {{ .Values.tls.certManager.existingIssuer.name }} 41 | kind: {{ .Values.tls.certManager.existingIssuer.kind }} 42 | {{- else }} 43 | name: {{ include "metrics-server.fullname" . }}-issuer 44 | kind: Issuer 45 | {{- end }} 46 | group: cert-manager.io 47 | {{- end }} 48 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/clusterrole-aggregated-reader.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ printf "system:%s-aggregated-reader" (include "metrics-server.name" .) }} 6 | labels: 7 | {{- include "metrics-server.labels" . | nindent 4 }} 8 | rbac.authorization.k8s.io/aggregate-to-admin: "true" 9 | rbac.authorization.k8s.io/aggregate-to-edit: "true" 10 | rbac.authorization.k8s.io/aggregate-to-view: "true" 11 | rules: 12 | - apiGroups: 13 | - metrics.k8s.io 14 | resources: 15 | - pods 16 | - nodes 17 | verbs: 18 | - get 19 | - list 20 | - watch 21 | {{- end -}} 22 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/clusterrole-nanny.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.create .Values.addonResizer.enabled -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ printf "system:%s-nanny" (include "metrics-server.fullname" .) }} 6 | labels: 7 | {{- include "metrics-server.labels" . | nindent 4 }} 8 | rules: 9 | - nonResourceURLs: 10 | - /metrics 11 | verbs: 12 | - get 13 | {{- end -}} 14 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ printf "system:%s" (include "metrics-server.fullname" .) }} 6 | labels: 7 | {{- include "metrics-server.labels" . | nindent 4 }} 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - nodes/metrics 13 | verbs: 14 | - get 15 | - apiGroups: 16 | - "" 17 | resources: 18 | - pods 19 | - nodes 20 | - namespaces 21 | - configmaps 22 | verbs: 23 | - get 24 | - list 25 | - watch 26 | {{- if .Values.rbac.pspEnabled }} 27 | - apiGroups: 28 | - extensions 29 | - policy 30 | resources: 31 | - podsecuritypolicies 32 | resourceNames: 33 | - {{ printf "privileged-%s" (include "metrics-server.fullname" .) }} 34 | verbs: 35 | - use 36 | {{- end -}} 37 | {{- end -}} 38 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/clusterrolebinding-auth-delegator.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: {{ printf "%s:system:auth-delegator" (include "metrics-server.fullname" .) }} 6 | labels: 7 | {{- include "metrics-server.labels" . | nindent 4 }} 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: system:auth-delegator 12 | subjects: 13 | - kind: ServiceAccount 14 | name: {{ include "metrics-server.serviceAccountName" . }} 15 | namespace: {{ .Release.Namespace }} 16 | {{- end -}} 17 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/clusterrolebinding-nanny.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | {{- if .Values.addonResizer.enabled -}} 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRoleBinding 5 | metadata: 6 | name: {{ printf "system:%s-nanny" (include "metrics-server.fullname" .) }} 7 | labels: 8 | {{- include "metrics-server.labels" . | nindent 4 }} 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: ClusterRole 12 | name: system:{{ template "metrics-server.fullname" . }}-nanny 13 | subjects: 14 | - kind: ServiceAccount 15 | name: {{ include "metrics-server.serviceAccountName" . }} 16 | namespace: {{ .Release.Namespace }} 17 | {{- end -}} 18 | {{- end -}} 19 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: {{ printf "system:%s" (include "metrics-server.fullname" .) }} 6 | labels: 7 | {{- include "metrics-server.labels" . | nindent 4 }} 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: system:{{ template "metrics-server.fullname" . }} 12 | subjects: 13 | - kind: ServiceAccount 14 | name: {{ include "metrics-server.serviceAccountName" . }} 15 | namespace: {{ .Release.Namespace }} 16 | {{- end -}} 17 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/configmaps-nanny.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.addonResizer.enabled -}} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ include "metrics-server.addonResizer.configMap" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "metrics-server.labels" . | nindent 4 }} 9 | data: 10 | NannyConfiguration: |- 11 | apiVersion: nannyconfig/v1alpha1 12 | kind: NannyConfiguration 13 | baseCPU: {{ .Values.addonResizer.nanny.cpu }} 14 | cpuPerNode: {{ .Values.addonResizer.nanny.extraCpu }} 15 | baseMemory: {{ .Values.addonResizer.nanny.memory }} 16 | memoryPerNode: {{ .Values.addonResizer.nanny.extraMemory }} 17 | {{- end -}} 18 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "metrics-server.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | {{- include "metrics-server.labels" . | nindent 4 }} 8 | {{- with .Values.deploymentAnnotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | spec: 13 | replicas: {{ .Values.replicas }} 14 | {{- if not (has (quote .Values.revisionHistoryLimit) (list "" (quote ""))) }} 15 | revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} 16 | {{- end }} 17 | {{- with .Values.updateStrategy }} 18 | strategy: 19 | {{- toYaml . | nindent 4 }} 20 | {{- end }} 21 | selector: 22 | matchLabels: 23 | {{- include "metrics-server.selectorLabels" . | nindent 6 }} 24 | template: 25 | metadata: 26 | labels: 27 | {{- include "metrics-server.selectorLabels" . | nindent 8 }} 28 | {{- with .Values.podLabels }} 29 | {{- toYaml . | nindent 8 }} 30 | {{- end }} 31 | {{- with .Values.podAnnotations }} 32 | annotations: 33 | {{- toYaml . | nindent 8 }} 34 | {{- end }} 35 | spec: 36 | {{- with .Values.schedulerName }} 37 | schedulerName: {{ . }} 38 | {{- end }} 39 | {{- with .Values.imagePullSecrets }} 40 | imagePullSecrets: 41 | {{- toYaml . | nindent 8 }} 42 | {{- end }} 43 | serviceAccountName: {{ include "metrics-server.serviceAccountName" . }} 44 | {{- with .Values.podSecurityContext }} 45 | securityContext: 46 | {{- toYaml . | nindent 8 }} 47 | {{- end }} 48 | {{- with .Values.priorityClassName }} 49 | priorityClassName: {{ . | quote }} 50 | {{- end }} 51 | {{- if .Values.hostNetwork.enabled }} 52 | hostNetwork: true 53 | {{- end }} 54 | {{- with .Values.dnsConfig }} 55 | dnsConfig: 56 | {{- toYaml . | nindent 8 }} 57 | {{- end }} 58 | containers: 59 | - name: metrics-server 60 | {{- with .Values.securityContext }} 61 | securityContext: 62 | {{- toYaml . | nindent 12 }} 63 | {{- end }} 64 | image: {{ include "metrics-server.image" . }} 65 | imagePullPolicy: {{ .Values.image.pullPolicy }} 66 | args: 67 | - {{ printf "--secure-port=%d" (int .Values.containerPort) }} 68 | {{- range .Values.defaultArgs }} 69 | - {{ . }} 70 | {{- end }} 71 | {{- if .Values.metrics.enabled }} 72 | - --authorization-always-allow-paths=/metrics 73 | {{- end }} 74 | {{- if ne .Values.tls.type "metrics-server" }} 75 | - --tls-cert-file=/tmp/tls-certs/tls.crt 76 | - --tls-private-key-file=/tmp/tls-certs/tls.key 77 | {{- end }} 78 | {{- range .Values.args }} 79 | - {{ . }} 80 | {{- end }} 81 | ports: 82 | - name: https 83 | protocol: TCP 84 | containerPort: {{ .Values.containerPort }} 85 | {{- with .Values.livenessProbe }} 86 | livenessProbe: 87 | {{- toYaml . | nindent 12 }} 88 | {{- end }} 89 | {{- with .Values.readinessProbe }} 90 | readinessProbe: 91 | {{- toYaml . | nindent 12 }} 92 | {{- end }} 93 | volumeMounts: 94 | - name: tmp 95 | mountPath: /tmp 96 | {{- if ne .Values.tls.type "metrics-server" }} 97 | - mountPath: /tmp/tls-certs 98 | name: certs 99 | readOnly: true 100 | {{- end }} 101 | {{- with .Values.extraVolumeMounts }} 102 | {{- toYaml . | nindent 12 }} 103 | {{- end }} 104 | {{- with .Values.resources }} 105 | resources: 106 | {{- toYaml . | nindent 12 }} 107 | {{- end }} 108 | {{- if .Values.addonResizer.enabled }} 109 | - name: metrics-server-nanny 110 | {{- with .Values.addonResizer.securityContext }} 111 | securityContext: 112 | {{- toYaml . | nindent 12 }} 113 | {{- end }} 114 | image: {{ include "metrics-server.addonResizer.image" . }} 115 | env: 116 | - name: MY_POD_NAME 117 | valueFrom: 118 | fieldRef: 119 | fieldPath: metadata.name 120 | - name: MY_POD_NAMESPACE 121 | valueFrom: 122 | fieldRef: 123 | fieldPath: metadata.namespace 124 | command: 125 | - /pod_nanny 126 | - --config-dir=/etc/config 127 | - --deployment={{ include "metrics-server.fullname" . }} 128 | - --container=metrics-server 129 | - --threshold={{ .Values.addonResizer.nanny.threshold }} 130 | - --poll-period={{ .Values.addonResizer.nanny.pollPeriod }} 131 | - --estimator=exponential 132 | - --minClusterSize={{ .Values.addonResizer.nanny.minClusterSize }} 133 | - --use-metrics=true 134 | volumeMounts: 135 | - name: nanny-config-volume 136 | mountPath: /etc/config 137 | {{- with .Values.addonResizer.resources }} 138 | resources: 139 | {{- toYaml . | nindent 12 }} 140 | {{- end }} 141 | {{- end }} 142 | volumes: 143 | - name: tmp 144 | {{- toYaml .Values.tmpVolume | nindent 10 }} 145 | {{- if .Values.addonResizer.enabled }} 146 | - name: nanny-config-volume 147 | configMap: 148 | name: {{ include "metrics-server.addonResizer.configMap" . }} 149 | {{- end }} 150 | {{- if ne .Values.tls.type "metrics-server" }} 151 | - name: certs 152 | secret: 153 | {{- if and (eq .Values.tls.type "existingSecret") .Values.tls.existingSecret.name }} 154 | secretName: {{ .Values.tls.existingSecret.name }} 155 | {{- else }} 156 | secretName: {{ include "metrics-server.fullname" . }} 157 | {{- end }} 158 | {{- end }} 159 | {{- with .Values.extraVolumes }} 160 | {{- toYaml . | nindent 8 }} 161 | {{- end }} 162 | {{- with .Values.nodeSelector }} 163 | nodeSelector: 164 | {{- toYaml . | nindent 8 }} 165 | {{- end }} 166 | {{- with .Values.affinity }} 167 | affinity: 168 | {{- toYaml . | nindent 8 }} 169 | {{- end }} 170 | {{- with .Values.tolerations }} 171 | tolerations: 172 | {{- toYaml . | nindent 8 }} 173 | {{- end }} 174 | {{- with .Values.topologySpreadConstraints }} 175 | topologySpreadConstraints: 176 | {{- toYaml . | nindent 8 }} 177 | {{- end }} 178 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/pdb.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.podDisruptionBudget.enabled -}} 2 | apiVersion: {{ include "metrics-server.pdb.apiVersion" . }} 3 | kind: PodDisruptionBudget 4 | metadata: 5 | name: {{ include "metrics-server.fullname" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "metrics-server.labels" . | nindent 4 }} 9 | spec: 10 | {{- if .Values.podDisruptionBudget.minAvailable }} 11 | minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} 12 | {{- end }} 13 | {{- if .Values.podDisruptionBudget.maxUnavailable }} 14 | maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} 15 | {{- end }} 16 | {{- if (semverCompare ">= 1.27-0" .Capabilities.KubeVersion.Version) }} 17 | {{- with .Values.podDisruptionBudget.unhealthyPodEvictionPolicy }} 18 | unhealthyPodEvictionPolicy: {{ . }} 19 | {{- end }} 20 | {{- end }} 21 | 22 | selector: 23 | matchLabels: 24 | {{- include "metrics-server.selectorLabels" . | nindent 6 }} 25 | {{- end -}} 26 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/psp.yaml: -------------------------------------------------------------------------------- 1 | {{- if and (.Values.rbac.pspEnabled) (semverCompare "<1.25-0" .Capabilities.KubeVersion.GitVersion) }} 2 | apiVersion: policy/v1beta1 3 | kind: PodSecurityPolicy 4 | metadata: 5 | name: {{ printf "privileged-%s" (include "metrics-server.fullname" .) }} 6 | labels: 7 | {{- include "metrics-server.labels" . | nindent 4 }} 8 | spec: 9 | allowedCapabilities: 10 | - '*' 11 | fsGroup: 12 | rule: RunAsAny 13 | privileged: true 14 | runAsUser: 15 | rule: RunAsAny 16 | seLinux: 17 | rule: RunAsAny 18 | supplementalGroups: 19 | rule: RunAsAny 20 | volumes: 21 | - '*' 22 | hostPID: true 23 | hostIPC: true 24 | hostNetwork: true 25 | hostPorts: 26 | - min: 1 27 | max: 65536 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/role-nanny.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | {{- if .Values.addonResizer.enabled -}} 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: Role 5 | metadata: 6 | name: {{ include "metrics-server.addonResizer.role" . }} 7 | namespace: {{ .Release.Namespace }} 8 | labels: 9 | {{- include "metrics-server.labels" . | nindent 4 }} 10 | rules: 11 | - apiGroups: 12 | - "" 13 | resources: 14 | - pods 15 | verbs: 16 | - get 17 | - apiGroups: 18 | - apps 19 | resources: 20 | - deployments 21 | resourceNames: 22 | - {{ include "metrics-server.fullname" . }} 23 | verbs: 24 | - get 25 | - patch 26 | {{- end -}} 27 | {{- end -}} 28 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/rolebinding-nanny.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | {{- if .Values.addonResizer.enabled -}} 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: RoleBinding 5 | metadata: 6 | name: {{ printf "%s-nanny" (include "metrics-server.fullname" .) }} 7 | namespace: {{ .Release.Namespace }} 8 | labels: 9 | {{- include "metrics-server.labels" . | nindent 4 }} 10 | roleRef: 11 | apiGroup: rbac.authorization.k8s.io 12 | kind: Role 13 | name: {{ include "metrics-server.addonResizer.role" . }} 14 | subjects: 15 | - kind: ServiceAccount 16 | name: {{ include "metrics-server.serviceAccountName" . }} 17 | namespace: {{ .Release.Namespace }} 18 | {{- end -}} 19 | {{- end -}} 20 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: RoleBinding 4 | metadata: 5 | name: {{ printf "%s-auth-reader" (include "metrics-server.fullname" .) }} 6 | namespace: kube-system 7 | labels: 8 | {{- include "metrics-server.labels" . | nindent 4 }} 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: Role 12 | name: extension-apiserver-authentication-reader 13 | subjects: 14 | - kind: ServiceAccount 15 | name: {{ include "metrics-server.serviceAccountName" . }} 16 | namespace: {{ .Release.Namespace }} 17 | {{- end -}} 18 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "metrics-server.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | {{- include "metrics-server.labels" . | nindent 4 }} 8 | {{- with .Values.service.labels -}} 9 | {{- toYaml . | nindent 4 }} 10 | {{- end }} 11 | {{- with .Values.service.annotations }} 12 | annotations: 13 | {{- toYaml . | nindent 4 }} 14 | {{- end }} 15 | spec: 16 | type: {{ .Values.service.type }} 17 | ports: 18 | - name: https 19 | port: {{ .Values.service.port }} 20 | protocol: TCP 21 | targetPort: https 22 | appProtocol: https 23 | selector: 24 | {{- include "metrics-server.selectorLabels" . | nindent 4 }} 25 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ template "metrics-server.serviceAccountName" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "metrics-server.labels" . | nindent 4 }} 9 | {{- with .Values.serviceAccount.annotations }} 10 | annotations: 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | {{- with .Values.serviceAccount.secrets }} 14 | secrets: 15 | {{- toYaml . | nindent 2 }} 16 | {{- end }} 17 | {{- end -}} 18 | -------------------------------------------------------------------------------- /charts/metrics-server/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.serviceMonitor.enabled .Values.metrics.enabled -}} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | name: {{ include "metrics-server.fullname" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "metrics-server.labels" . | nindent 4 }} 9 | {{- with .Values.serviceMonitor.additionalLabels }} 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | spec: 13 | jobLabel: app.kubernetes.io/instance 14 | namespaceSelector: 15 | matchNames: 16 | - {{ .Release.Namespace }} 17 | selector: 18 | matchLabels: 19 | {{- include "metrics-server.selectorLabels" . | nindent 6 }} 20 | endpoints: 21 | - port: https 22 | path: /metrics 23 | scheme: https 24 | tlsConfig: 25 | insecureSkipVerify: true 26 | {{- with .Values.serviceMonitor.interval }} 27 | interval: {{ . }} 28 | {{- end }} 29 | {{- with .Values.serviceMonitor.scrapeTimeout }} 30 | scrapeTimeout: {{ . }} 31 | {{- end }} 32 | {{- with .Values.serviceMonitor.metricRelabelings }} 33 | metricRelabelings: 34 | {{- toYaml . | nindent 8 }} 35 | {{- end }} 36 | {{- with .Values.serviceMonitor.relabelings }} 37 | relabelings: 38 | {{- toYaml . | nindent 8 }} 39 | {{- end }} 40 | {{- end -}} 41 | -------------------------------------------------------------------------------- /charts/metrics-server/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for metrics-server. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | image: 6 | repository: registry.k8s.io/metrics-server/metrics-server 7 | # Overrides the image tag whose default is v{{ .Chart.AppVersion }} 8 | tag: "" 9 | pullPolicy: IfNotPresent 10 | 11 | imagePullSecrets: [] 12 | # - name: registrySecretName 13 | 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: true 20 | # Annotations to add to the service account 21 | annotations: {} 22 | # The name of the service account to use. 23 | # If not set and create is true, a name is generated using the fullname template 24 | name: "" 25 | # The list of secrets mountable by this service account. 26 | # See https://kubernetes.io/docs/reference/labels-annotations-taints/#enforce-mountable-secrets 27 | secrets: [] 28 | 29 | rbac: 30 | # Specifies whether RBAC resources should be created 31 | create: true 32 | # Note: PodSecurityPolicy will not be created when Kubernetes version is 1.25 or later. 33 | pspEnabled: false 34 | 35 | apiService: 36 | # Specifies if the v1beta1.metrics.k8s.io API service should be created. 37 | # 38 | # You typically want this enabled! If you disable API service creation you have to 39 | # manage it outside of this chart for e.g horizontal pod autoscaling to 40 | # work with this release. 41 | create: true 42 | # Annotations to add to the API service 43 | annotations: {} 44 | # Specifies whether to skip TLS verification 45 | insecureSkipTLSVerify: true 46 | # The PEM encoded CA bundle for TLS verification 47 | caBundle: "" 48 | 49 | commonLabels: {} 50 | podLabels: {} 51 | podAnnotations: {} 52 | 53 | podSecurityContext: {} 54 | 55 | securityContext: 56 | allowPrivilegeEscalation: false 57 | readOnlyRootFilesystem: true 58 | runAsNonRoot: true 59 | runAsUser: 1000 60 | seccompProfile: 61 | type: RuntimeDefault 62 | capabilities: 63 | drop: 64 | - ALL 65 | 66 | priorityClassName: system-cluster-critical 67 | 68 | containerPort: 10250 69 | 70 | hostNetwork: 71 | # Specifies if metrics-server should be started in hostNetwork mode. 72 | # 73 | # You would require this enabled if you use alternate overlay networking for pods and 74 | # API server unable to communicate with metrics-server. As an example, this is required 75 | # if you use Weave network on EKS 76 | enabled: false 77 | 78 | replicas: 1 79 | 80 | revisionHistoryLimit: 81 | 82 | updateStrategy: {} 83 | # type: RollingUpdate 84 | # rollingUpdate: 85 | # maxSurge: 0 86 | # maxUnavailable: 1 87 | 88 | podDisruptionBudget: 89 | # https://kubernetes.io/docs/tasks/run-application/configure-pdb/ 90 | enabled: false 91 | minAvailable: 92 | maxUnavailable: 93 | unhealthyPodEvictionPolicy: 94 | 95 | defaultArgs: 96 | - --cert-dir=/tmp 97 | - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname 98 | - --kubelet-use-node-status-port 99 | - --metric-resolution=15s 100 | 101 | args: [] 102 | 103 | livenessProbe: 104 | httpGet: 105 | path: /livez 106 | port: https 107 | scheme: HTTPS 108 | initialDelaySeconds: 0 109 | periodSeconds: 10 110 | failureThreshold: 3 111 | 112 | readinessProbe: 113 | httpGet: 114 | path: /readyz 115 | port: https 116 | scheme: HTTPS 117 | initialDelaySeconds: 20 118 | periodSeconds: 10 119 | failureThreshold: 3 120 | 121 | service: 122 | type: ClusterIP 123 | port: 443 124 | annotations: {} 125 | labels: {} 126 | # Add these labels to have metrics-server show up in `kubectl cluster-info` 127 | # kubernetes.io/cluster-service: "true" 128 | # kubernetes.io/name: "Metrics-server" 129 | 130 | addonResizer: 131 | enabled: false 132 | image: 133 | repository: registry.k8s.io/autoscaling/addon-resizer 134 | tag: 1.8.23 135 | securityContext: 136 | allowPrivilegeEscalation: false 137 | readOnlyRootFilesystem: true 138 | runAsNonRoot: true 139 | runAsUser: 1000 140 | seccompProfile: 141 | type: RuntimeDefault 142 | capabilities: 143 | drop: 144 | - ALL 145 | resources: 146 | requests: 147 | cpu: 40m 148 | memory: 25Mi 149 | limits: 150 | cpu: 40m 151 | memory: 25Mi 152 | nanny: 153 | cpu: 0m 154 | extraCpu: 1m 155 | memory: 0Mi 156 | extraMemory: 2Mi 157 | minClusterSize: 100 158 | pollPeriod: 300000 159 | threshold: 5 160 | 161 | metrics: 162 | enabled: false 163 | 164 | serviceMonitor: 165 | enabled: false 166 | additionalLabels: {} 167 | interval: 1m 168 | scrapeTimeout: 10s 169 | metricRelabelings: [] 170 | relabelings: [] 171 | 172 | # See https://github.com/kubernetes-sigs/metrics-server#scaling 173 | resources: 174 | requests: 175 | cpu: 100m 176 | memory: 200Mi 177 | # limits: 178 | # cpu: 179 | # memory: 180 | 181 | extraVolumeMounts: [] 182 | 183 | extraVolumes: [] 184 | 185 | nodeSelector: {} 186 | 187 | tolerations: [] 188 | 189 | affinity: {} 190 | 191 | topologySpreadConstraints: [] 192 | 193 | dnsConfig: {} 194 | 195 | # Annotations to add to the deployment 196 | deploymentAnnotations: {} 197 | 198 | schedulerName: "" 199 | 200 | tmpVolume: 201 | emptyDir: {} 202 | 203 | tls: 204 | # Set the TLS method to use. Supported values: 205 | # - `metrics-server` : Metrics-server will generate a self-signed certificate 206 | # - `helm` : Helm will generate a self-signed certificate 207 | # - `cert-manager` : Use cert-manager.io to create and maintain the certificate 208 | # - `existingSecret` : Reuse an existing secret. No new secret will be created 209 | type: "metrics-server" 210 | # Kubernetes cluster domain. Used to configure Subject Alt Names for the certificate 211 | clusterDomain: cluster.local 212 | 213 | certManager: 214 | # Automatically add the cert-manager.io/inject-ca-from annotation to the APIService resource. 215 | # See https://cert-manager.io/docs/concepts/ca-injector 216 | addInjectorAnnotations: true 217 | existingIssuer: 218 | # Use an existing cert-manager issuer 219 | enabled: false 220 | # Kind of the existing cert-manager issuer 221 | kind: "Issuer" 222 | # Name of the existing cert-manager issuer 223 | name: "my-issuer" 224 | # Set the requested duration (i.e. lifetime) of the Certificate. 225 | # See https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec 226 | duration: "" 227 | # How long before the currently issued certificate’s expiry cert-manager should renew the certificate. 228 | # See https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec 229 | renewBefore: "" 230 | # Add extra annotations to the Certificate resource 231 | annotations: {} 232 | # Add extra labels to the Certificate resource 233 | labels: {} 234 | 235 | helm: 236 | # Use helm lookup function to reuse Secret created in previous helm install 237 | lookup: true 238 | # Cert validity duration in days 239 | certDurationDays: 365 240 | 241 | existingSecret: 242 | # Name of the existing Secret to use for TLS 243 | name: "" 244 | # Use helm lookup function to provision `apiService.caBundle` 245 | lookup: true 246 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | # See https://cloud.google.com/cloud-build/docs/build-config 2 | timeout: 3600s 3 | options: 4 | substitution_option: ALLOW_LOOSE 5 | steps: 6 | - name: 'gcr.io/k8s-staging-test-infra/gcb-docker-gcloud:v20231105-52c482caa0' 7 | entrypoint: make 8 | env: 9 | - GIT_TAG=$_PULL_BASE_REF 10 | - GIT_COMMIT=$_PULL_BASE_SHA 11 | args: 12 | - push-all 13 | -------------------------------------------------------------------------------- /cmd/metrics-server/app/options/kubelet_client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package options 15 | 16 | import ( 17 | "fmt" 18 | "time" 19 | 20 | "github.com/spf13/pflag" 21 | 22 | corev1 "k8s.io/api/core/v1" 23 | "k8s.io/client-go/rest" 24 | 25 | "sigs.k8s.io/metrics-server/pkg/scraper/client" 26 | "sigs.k8s.io/metrics-server/pkg/utils" 27 | ) 28 | 29 | type KubeletClientOptions struct { 30 | KubeletUseNodeStatusPort bool 31 | KubeletPort int 32 | InsecureKubeletTLS bool 33 | KubeletPreferredAddressTypes []string 34 | KubeletCAFile string 35 | KubeletClientKeyFile string 36 | KubeletClientCertFile string 37 | DeprecatedCompletelyInsecureKubelet bool 38 | KubeletRequestTimeout time.Duration 39 | NodeSelector string 40 | } 41 | 42 | func (o *KubeletClientOptions) Validate() []error { 43 | errors := []error{} 44 | if (o.KubeletCAFile != "") && o.InsecureKubeletTLS { 45 | errors = append(errors, fmt.Errorf("cannot use both --kubelet-certificate-authority and --kubelet-insecure-tls")) 46 | } 47 | 48 | if (o.KubeletClientKeyFile != "") != (o.KubeletClientCertFile != "") { 49 | errors = append(errors, fmt.Errorf("need both --kubelet-client-key and --kubelet-client-certificate")) 50 | } 51 | 52 | if (o.KubeletClientKeyFile != "") && o.DeprecatedCompletelyInsecureKubelet { 53 | errors = append(errors, fmt.Errorf("cannot use both --kubelet-client-key and --deprecated-kubelet-completely-insecure")) 54 | } 55 | 56 | if (o.KubeletClientCertFile != "") && o.DeprecatedCompletelyInsecureKubelet { 57 | errors = append(errors, fmt.Errorf("cannot use both --kubelet-client-certificate and --deprecated-kubelet-completely-insecure")) 58 | } 59 | 60 | if o.InsecureKubeletTLS && o.DeprecatedCompletelyInsecureKubelet { 61 | errors = append(errors, fmt.Errorf("cannot use both --kubelet-insecure-tls and --deprecated-kubelet-completely-insecure")) 62 | } 63 | if (o.KubeletCAFile != "") && o.DeprecatedCompletelyInsecureKubelet { 64 | errors = append(errors, fmt.Errorf("cannot use both --kubelet-certificate-authority and --deprecated-kubelet-completely-insecure")) 65 | } 66 | if o.KubeletRequestTimeout <= 0 { 67 | errors = append(errors, fmt.Errorf("kubelet-request-timeout should be positive")) 68 | } 69 | return errors 70 | } 71 | 72 | func (o *KubeletClientOptions) AddFlags(fs *pflag.FlagSet) { 73 | fs.BoolVar(&o.InsecureKubeletTLS, "kubelet-insecure-tls", o.InsecureKubeletTLS, "Do not verify CA of serving certificates presented by Kubelets. For testing purposes only.") 74 | fs.BoolVar(&o.KubeletUseNodeStatusPort, "kubelet-use-node-status-port", o.KubeletUseNodeStatusPort, "Use the port in the node status. Takes precedence over --kubelet-port flag.") 75 | fs.IntVar(&o.KubeletPort, "kubelet-port", o.KubeletPort, "The port to use to connect to Kubelets.") 76 | fs.StringSliceVar(&o.KubeletPreferredAddressTypes, "kubelet-preferred-address-types", o.KubeletPreferredAddressTypes, "The priority of node address types to use when determining which address to use to connect to a particular node") 77 | fs.StringVar(&o.KubeletCAFile, "kubelet-certificate-authority", "", "Path to the CA to use to validate the Kubelet's serving certificates.") 78 | fs.StringVar(&o.KubeletClientKeyFile, "kubelet-client-key", "", "Path to a client key file for TLS.") 79 | fs.StringVar(&o.KubeletClientCertFile, "kubelet-client-certificate", "", "Path to a client cert file for TLS.") 80 | fs.DurationVar(&o.KubeletRequestTimeout, "kubelet-request-timeout", o.KubeletRequestTimeout, "The length of time to wait before giving up on a single request to Kubelet. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h).") 81 | fs.StringVarP(&o.NodeSelector, "node-selector", "l", o.NodeSelector, "Selector (label query) to filter on, not including uninitialized ones, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2).") 82 | // MarkDeprecated hides the flag from the help. We don't want that. 83 | fs.BoolVar(&o.DeprecatedCompletelyInsecureKubelet, "deprecated-kubelet-completely-insecure", o.DeprecatedCompletelyInsecureKubelet, "DEPRECATED: Do not use any encryption, authorization, or authentication when communicating with the Kubelet. This is rarely the right option, since it leaves kubelet communication completely insecure. If you encounter auth errors, make sure you've enabled token webhook auth on the Kubelet, and if you're in a test cluster with self-signed Kubelet certificates, consider using kubelet-insecure-tls instead.") 84 | } 85 | 86 | // NewKubeletClientOptions constructs a new set of default options for metrics-server. 87 | func NewKubeletClientOptions() *KubeletClientOptions { 88 | o := &KubeletClientOptions{ 89 | KubeletPort: 10250, 90 | KubeletPreferredAddressTypes: make([]string, len(utils.DefaultAddressTypePriority)), 91 | KubeletRequestTimeout: 10 * time.Second, 92 | } 93 | 94 | for i, addrType := range utils.DefaultAddressTypePriority { 95 | o.KubeletPreferredAddressTypes[i] = string(addrType) 96 | } 97 | 98 | return o 99 | } 100 | 101 | func (o KubeletClientOptions) Config(restConfig *rest.Config) *client.KubeletClientConfig { 102 | config := &client.KubeletClientConfig{ 103 | Scheme: "https", 104 | DefaultPort: o.KubeletPort, 105 | AddressTypePriority: o.addressResolverConfig(), 106 | UseNodeStatusPort: o.KubeletUseNodeStatusPort, 107 | Client: *rest.CopyConfig(restConfig), 108 | } 109 | if o.DeprecatedCompletelyInsecureKubelet { 110 | config.Scheme = "http" 111 | config.Client = *rest.AnonymousClientConfig(&config.Client) // don't use auth to avoid leaking auth details to insecure endpoints 112 | config.Client.TLSClientConfig = rest.TLSClientConfig{} // empty TLS config --> no TLS 113 | } 114 | if o.InsecureKubeletTLS { 115 | config.Client.TLSClientConfig.Insecure = true 116 | config.Client.TLSClientConfig.CAData = nil 117 | config.Client.TLSClientConfig.CAFile = "" 118 | } 119 | if len(o.KubeletCAFile) > 0 { 120 | config.Client.TLSClientConfig.CAFile = o.KubeletCAFile 121 | config.Client.TLSClientConfig.CAData = nil 122 | } 123 | if len(o.KubeletClientCertFile) > 0 { 124 | config.Client.TLSClientConfig.CertFile = o.KubeletClientCertFile 125 | config.Client.TLSClientConfig.CertData = nil 126 | } 127 | if len(o.KubeletClientKeyFile) > 0 { 128 | config.Client.TLSClientConfig.KeyFile = o.KubeletClientKeyFile 129 | config.Client.TLSClientConfig.KeyData = nil 130 | } 131 | return config 132 | } 133 | 134 | func (o KubeletClientOptions) addressResolverConfig() []corev1.NodeAddressType { 135 | addrPriority := make([]corev1.NodeAddressType, len(o.KubeletPreferredAddressTypes)) 136 | for i, addrType := range o.KubeletPreferredAddressTypes { 137 | addrPriority[i] = corev1.NodeAddressType(addrType) 138 | } 139 | return addrPriority 140 | } 141 | -------------------------------------------------------------------------------- /cmd/metrics-server/app/options/options_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package options 15 | 16 | import ( 17 | "testing" 18 | "time" 19 | 20 | "k8s.io/component-base/logs" 21 | ) 22 | 23 | func TestOptions_validate(t *testing.T) { 24 | for _, tc := range []struct { 25 | name string 26 | options *Options 27 | expectedErrorCount int 28 | }{ 29 | { 30 | name: "can give --metric-resolution larger than --kubelet-request-timeout", 31 | options: &Options{ 32 | MetricResolution: 10 * time.Second, 33 | KubeletClient: &KubeletClientOptions{KubeletRequestTimeout: 9 * time.Second}, 34 | Logging: logs.NewOptions(), 35 | }, 36 | expectedErrorCount: 0, 37 | }, 38 | { 39 | name: "can not give --metric-resolution * 9/10 less than --kubelet-request-timeout", 40 | options: &Options{ 41 | MetricResolution: 10 * time.Second, 42 | KubeletClient: &KubeletClientOptions{KubeletRequestTimeout: 10 * time.Second}, 43 | Logging: logs.NewOptions(), 44 | }, 45 | expectedErrorCount: 1, 46 | }, 47 | } { 48 | t.Run(tc.name, func(t *testing.T) { 49 | errors := tc.options.validate() 50 | if len(errors) != tc.expectedErrorCount { 51 | t.Errorf("options.Validate() = %q, expected length %d", errors, tc.expectedErrorCount) 52 | } 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /cmd/metrics-server/app/start.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package app 16 | 17 | import ( 18 | "flag" 19 | "fmt" 20 | "os" 21 | 22 | "github.com/spf13/cobra" 23 | 24 | "k8s.io/client-go/pkg/version" 25 | cliflag "k8s.io/component-base/cli/flag" 26 | "k8s.io/component-base/logs" 27 | "k8s.io/component-base/term" 28 | 29 | "sigs.k8s.io/metrics-server/cmd/metrics-server/app/options" 30 | ) 31 | 32 | // NewMetricsServerCommand provides a CLI handler for the metrics server entrypoint 33 | func NewMetricsServerCommand(stopCh <-chan struct{}) *cobra.Command { 34 | opts := options.NewOptions() 35 | cmd := &cobra.Command{ 36 | Short: "Launch metrics-server", 37 | Long: "Launch metrics-server", 38 | RunE: func(c *cobra.Command, args []string) error { 39 | if err := runCommand(opts, stopCh); err != nil { 40 | return err 41 | } 42 | return nil 43 | }, 44 | } 45 | fs := cmd.Flags() 46 | nfs := opts.Flags() 47 | for _, f := range nfs.FlagSets { 48 | fs.AddFlagSet(f) 49 | } 50 | local := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 51 | logs.AddGoFlags(local) 52 | nfs.FlagSet("logging").AddGoFlagSet(local) 53 | 54 | usageFmt := "Usage:\n %s\n" 55 | cols, _, _ := term.TerminalSize(cmd.OutOrStdout()) 56 | cmd.SetUsageFunc(func(cmd *cobra.Command) error { 57 | fmt.Fprintf(cmd.OutOrStderr(), usageFmt, cmd.UseLine()) 58 | cliflag.PrintSections(cmd.OutOrStderr(), nfs, cols) 59 | return nil 60 | }) 61 | cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { 62 | fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n"+usageFmt, cmd.Long, cmd.UseLine()) 63 | cliflag.PrintSections(cmd.OutOrStdout(), nfs, cols) 64 | }) 65 | fs.AddGoFlagSet(local) 66 | return cmd 67 | } 68 | 69 | func runCommand(o *options.Options, stopCh <-chan struct{}) error { 70 | if o.ShowVersion { 71 | fmt.Println(version.Get().GitVersion) 72 | os.Exit(0) 73 | } 74 | 75 | errors := o.Validate() 76 | if len(errors) > 0 { 77 | return errors[0] 78 | } 79 | 80 | config, err := o.ServerConfig() 81 | 82 | if err != nil { 83 | return err 84 | } 85 | 86 | s, err := config.Complete() 87 | 88 | if err != nil { 89 | return err 90 | } 91 | 92 | return s.RunUntil(stopCh) 93 | } 94 | -------------------------------------------------------------------------------- /cmd/metrics-server/metrics-server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "os" 19 | "runtime" 20 | 21 | genericapiserver "k8s.io/apiserver/pkg/server" 22 | "k8s.io/component-base/logs" 23 | 24 | "sigs.k8s.io/metrics-server/cmd/metrics-server/app" 25 | ) 26 | 27 | func main() { 28 | logs.InitLogs() 29 | defer logs.FlushLogs() 30 | 31 | if len(os.Getenv("GOMAXPROCS")) == 0 { 32 | runtime.GOMAXPROCS(runtime.NumCPU()) 33 | } 34 | 35 | cmd := app.NewMetricsServerCommand(genericapiserver.SetupSignalHandler()) 36 | if err := cmd.Execute(); err != nil { 37 | panic(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Community Code of Conduct 2 | 3 | Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module sigs.k8s.io/metrics-server 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/google/go-cmp v0.7.0 7 | github.com/onsi/ginkgo/v2 v2.21.0 8 | github.com/onsi/gomega v1.35.1 9 | github.com/prometheus/common v0.63.0 10 | github.com/prometheus/prometheus v0.303.1 11 | github.com/spf13/cobra v1.8.1 12 | github.com/spf13/pflag v1.0.5 13 | k8s.io/api v0.33.0 14 | k8s.io/apimachinery v0.33.0 15 | k8s.io/apiserver v0.33.0 16 | k8s.io/client-go v0.33.0 17 | k8s.io/component-base v0.33.0 18 | k8s.io/klog/v2 v2.130.1 19 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff 20 | k8s.io/metrics v0.33.0 21 | ) 22 | 23 | require ( 24 | cel.dev/expr v0.19.1 // indirect 25 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 26 | github.com/NYTimes/gziphandler v1.1.1 // indirect 27 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 28 | github.com/beorn7/perks v1.0.1 // indirect 29 | github.com/blang/semver/v4 v4.0.0 // indirect 30 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 31 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 32 | github.com/coreos/go-semver v0.3.1 // indirect 33 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 34 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 35 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 36 | github.com/felixge/httpsnoop v1.0.4 // indirect 37 | github.com/fsnotify/fsnotify v1.8.0 // indirect 38 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 39 | github.com/go-logr/logr v1.4.2 // indirect 40 | github.com/go-logr/stdr v1.2.2 // indirect 41 | github.com/go-logr/zapr v1.3.0 // indirect 42 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 43 | github.com/go-openapi/jsonreference v0.21.0 // indirect 44 | github.com/go-openapi/swag v0.23.0 // indirect 45 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 46 | github.com/gogo/protobuf v1.3.2 // indirect 47 | github.com/golang/protobuf v1.5.4 // indirect 48 | github.com/google/btree v1.1.3 // indirect 49 | github.com/google/cel-go v0.23.2 // indirect 50 | github.com/google/gnostic-models v0.6.9 // indirect 51 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect 52 | github.com/google/uuid v1.6.0 // indirect 53 | github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect 54 | github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect 55 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 56 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect 57 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 58 | github.com/josharian/intern v1.0.0 // indirect 59 | github.com/json-iterator/go v1.1.12 // indirect 60 | github.com/kylelemons/godebug v1.1.0 // indirect 61 | github.com/mailru/easyjson v0.7.7 // indirect 62 | github.com/moby/spdystream v0.5.0 // indirect 63 | github.com/moby/term v0.5.0 // indirect 64 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 65 | github.com/modern-go/reflect2 v1.0.2 // indirect 66 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 67 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 68 | github.com/pkg/errors v0.9.1 // indirect 69 | github.com/prometheus/client_golang v1.22.0 // indirect 70 | github.com/prometheus/client_model v0.6.1 // indirect 71 | github.com/prometheus/procfs v0.15.1 // indirect 72 | github.com/stoewer/go-strcase v1.3.0 // indirect 73 | github.com/x448/float16 v0.8.4 // indirect 74 | go.etcd.io/etcd/api/v3 v3.5.21 // indirect 75 | go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect 76 | go.etcd.io/etcd/client/v3 v3.5.21 // indirect 77 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 78 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect 79 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 80 | go.opentelemetry.io/otel v1.35.0 // indirect 81 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect 82 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect 83 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 84 | go.opentelemetry.io/otel/sdk v1.35.0 // indirect 85 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 86 | go.opentelemetry.io/proto/otlp v1.5.0 // indirect 87 | go.uber.org/multierr v1.11.0 // indirect 88 | go.uber.org/zap v1.27.0 // indirect 89 | golang.org/x/crypto v0.36.0 // indirect 90 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect 91 | golang.org/x/net v0.38.0 // indirect 92 | golang.org/x/oauth2 v0.27.0 // indirect 93 | golang.org/x/sync v0.12.0 // indirect 94 | golang.org/x/sys v0.31.0 // indirect 95 | golang.org/x/term v0.30.0 // indirect 96 | golang.org/x/text v0.23.0 // indirect 97 | golang.org/x/time v0.10.0 // indirect 98 | golang.org/x/tools v0.30.0 // indirect 99 | google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect 100 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e // indirect 101 | google.golang.org/grpc v1.71.0 // indirect 102 | google.golang.org/protobuf v1.36.5 // indirect 103 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 104 | gopkg.in/inf.v0 v0.9.1 // indirect 105 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 106 | gopkg.in/yaml.v3 v3.0.1 // indirect 107 | k8s.io/kms v0.33.0 // indirect 108 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 109 | sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect 110 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 111 | sigs.k8s.io/randfill v1.0.0 // indirect 112 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 113 | sigs.k8s.io/yaml v1.4.0 // indirect 114 | ) 115 | -------------------------------------------------------------------------------- /manifests/base/apiservice.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiregistration.k8s.io/v1 3 | kind: APIService 4 | metadata: 5 | name: v1beta1.metrics.k8s.io 6 | spec: 7 | service: 8 | name: metrics-server 9 | namespace: kube-system 10 | group: metrics.k8s.io 11 | version: v1beta1 12 | insecureSkipTLSVerify: true 13 | groupPriorityMinimum: 100 14 | versionPriority: 100 15 | -------------------------------------------------------------------------------- /manifests/base/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: metrics-server 6 | namespace: kube-system 7 | spec: 8 | strategy: 9 | rollingUpdate: 10 | maxUnavailable: 0 11 | template: 12 | spec: 13 | serviceAccountName: metrics-server 14 | volumes: 15 | # mount in tmp so we can safely use from-scratch images and/or read-only containers 16 | - name: tmp-dir 17 | emptyDir: {} 18 | priorityClassName: system-cluster-critical 19 | containers: 20 | - name: metrics-server 21 | image: gcr.io/k8s-staging-metrics-server/metrics-server:master 22 | imagePullPolicy: IfNotPresent 23 | args: 24 | - --cert-dir=/tmp 25 | - --secure-port=10250 26 | - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname 27 | - --kubelet-use-node-status-port 28 | - --metric-resolution=15s 29 | resources: 30 | requests: 31 | cpu: 100m 32 | memory: 200Mi 33 | ports: 34 | - name: https 35 | containerPort: 10250 36 | protocol: TCP 37 | readinessProbe: 38 | httpGet: 39 | path: /readyz 40 | port: https 41 | scheme: HTTPS 42 | periodSeconds: 10 43 | failureThreshold: 3 44 | initialDelaySeconds: 20 45 | livenessProbe: 46 | httpGet: 47 | path: /livez 48 | port: https 49 | scheme: HTTPS 50 | periodSeconds: 10 51 | failureThreshold: 3 52 | securityContext: 53 | readOnlyRootFilesystem: true 54 | runAsNonRoot: true 55 | runAsUser: 1000 56 | allowPrivilegeEscalation: false 57 | seccompProfile: 58 | type: RuntimeDefault 59 | capabilities: 60 | drop: 61 | - ALL 62 | volumeMounts: 63 | - name: tmp-dir 64 | mountPath: /tmp 65 | nodeSelector: 66 | kubernetes.io/os: linux 67 | -------------------------------------------------------------------------------- /manifests/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | commonLabels: 4 | k8s-app: metrics-server 5 | resources: 6 | - apiservice.yaml 7 | - deployment.yaml 8 | - rbac.yaml 9 | - service.yaml -------------------------------------------------------------------------------- /manifests/base/rbac.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: system:aggregated-metrics-reader 6 | labels: 7 | rbac.authorization.k8s.io/aggregate-to-view: "true" 8 | rbac.authorization.k8s.io/aggregate-to-edit: "true" 9 | rbac.authorization.k8s.io/aggregate-to-admin: "true" 10 | rules: 11 | - apiGroups: ["metrics.k8s.io"] 12 | resources: ["pods", "nodes"] 13 | verbs: ["get", "list", "watch"] 14 | --- 15 | apiVersion: v1 16 | kind: ServiceAccount 17 | metadata: 18 | name: metrics-server 19 | namespace: kube-system 20 | --- 21 | apiVersion: rbac.authorization.k8s.io/v1 22 | kind: RoleBinding 23 | metadata: 24 | name: metrics-server-auth-reader 25 | namespace: kube-system 26 | roleRef: 27 | apiGroup: rbac.authorization.k8s.io 28 | kind: Role 29 | name: extension-apiserver-authentication-reader 30 | subjects: 31 | - kind: ServiceAccount 32 | name: metrics-server 33 | namespace: kube-system 34 | --- 35 | apiVersion: rbac.authorization.k8s.io/v1 36 | kind: ClusterRoleBinding 37 | metadata: 38 | name: metrics-server:system:auth-delegator 39 | roleRef: 40 | apiGroup: rbac.authorization.k8s.io 41 | kind: ClusterRole 42 | name: system:auth-delegator 43 | subjects: 44 | - kind: ServiceAccount 45 | name: metrics-server 46 | namespace: kube-system 47 | --- 48 | apiVersion: rbac.authorization.k8s.io/v1 49 | kind: ClusterRole 50 | metadata: 51 | name: system:metrics-server 52 | rules: 53 | - apiGroups: [""] 54 | resources: 55 | - nodes/metrics 56 | verbs: 57 | - get 58 | - apiGroups: [""] 59 | resources: 60 | - pods 61 | - nodes 62 | verbs: 63 | - get 64 | - list 65 | - watch 66 | --- 67 | apiVersion: rbac.authorization.k8s.io/v1 68 | kind: ClusterRoleBinding 69 | metadata: 70 | name: system:metrics-server 71 | roleRef: 72 | apiGroup: rbac.authorization.k8s.io 73 | kind: ClusterRole 74 | name: system:metrics-server 75 | subjects: 76 | - kind: ServiceAccount 77 | name: metrics-server 78 | namespace: kube-system 79 | -------------------------------------------------------------------------------- /manifests/base/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: metrics-server 6 | namespace: kube-system 7 | spec: 8 | ports: 9 | - name: https 10 | port: 443 11 | protocol: TCP 12 | targetPort: https 13 | appProtocol: https 14 | -------------------------------------------------------------------------------- /manifests/components/autoscale/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | resources: 4 | - rbac.yaml 5 | patchesStrategicMerge: 6 | - patch.yaml 7 | -------------------------------------------------------------------------------- /manifests/components/autoscale/patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: metrics-server 5 | namespace: kube-system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: metrics-server 11 | # Resources are set by the nanny sidecar 12 | resources: 13 | $patch: delete 14 | - name: metrics-server-nanny 15 | securityContext: 16 | allowPrivilegeEscalation: false 17 | readOnlyRootFilesystem: true 18 | runAsNonRoot: true 19 | runAsUser: 1000 20 | seccompProfile: 21 | type: RuntimeDefault 22 | capabilities: 23 | drop: 24 | - ALL 25 | image: registry.k8s.io/autoscaling/addon-resizer:1.8.23 26 | resources: 27 | limits: 28 | cpu: 40m 29 | memory: 25Mi 30 | requests: 31 | cpu: 40m 32 | memory: 25Mi 33 | env: 34 | - name: MY_POD_NAME 35 | valueFrom: 36 | fieldRef: 37 | fieldPath: metadata.name 38 | - name: MY_POD_NAMESPACE 39 | valueFrom: 40 | fieldRef: 41 | fieldPath: metadata.namespace 42 | command: 43 | - /pod_nanny 44 | - --cpu=0m 45 | - --extra-cpu=1m 46 | - --memory=0Mi 47 | - --extra-memory=2Mi 48 | - --threshold=5 49 | - --deployment=metrics-server 50 | - --container=metrics-server 51 | - --poll-period=300000 52 | - --estimator=exponential 53 | - --minClusterSize=100 54 | - --use-metrics=true 55 | -------------------------------------------------------------------------------- /manifests/components/autoscale/rbac.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: system:metrics-server-nanny 6 | labels: 7 | k8s-app: metrics-server 8 | rules: 9 | - nonResourceURLs: 10 | - /metrics 11 | verbs: 12 | - get 13 | --- 14 | apiVersion: rbac.authorization.k8s.io/v1 15 | kind: ClusterRoleBinding 16 | metadata: 17 | name: system:metrics-server-nanny 18 | labels: 19 | k8s-app: metrics-server 20 | roleRef: 21 | apiGroup: rbac.authorization.k8s.io 22 | kind: ClusterRole 23 | name: system:metrics-server-nanny 24 | subjects: 25 | - kind: ServiceAccount 26 | name: metrics-server 27 | namespace: kube-system 28 | --- 29 | apiVersion: rbac.authorization.k8s.io/v1 30 | kind: Role 31 | metadata: 32 | name: metrics-server-nanny 33 | namespace: kube-system 34 | labels: 35 | k8s-app: metrics-server 36 | rules: 37 | - apiGroups: 38 | - "" 39 | resources: 40 | - pods 41 | verbs: 42 | - get 43 | - apiGroups: 44 | - apps 45 | resources: 46 | - deployments 47 | resourceNames: 48 | - metrics-server 49 | verbs: 50 | - get 51 | - patch 52 | --- 53 | apiVersion: rbac.authorization.k8s.io/v1 54 | kind: RoleBinding 55 | metadata: 56 | name: metrics-server-nanny 57 | namespace: kube-system 58 | labels: 59 | k8s-app: metrics-server 60 | roleRef: 61 | apiGroup: rbac.authorization.k8s.io 62 | kind: Role 63 | name: metrics-server-nanny 64 | subjects: 65 | - kind: ServiceAccount 66 | name: metrics-server 67 | namespace: kube-system 68 | -------------------------------------------------------------------------------- /manifests/components/high-availability-1.21+/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | components: 4 | - ../high-availability 5 | patches: 6 | - path: patch-pdb-version.yaml 7 | target: 8 | kind: PodDisruptionBudget 9 | -------------------------------------------------------------------------------- /manifests/components/high-availability-1.21+/patch-pdb-version.yaml: -------------------------------------------------------------------------------- 1 | - op: replace 2 | path: "/apiVersion" 3 | value: policy/v1 4 | -------------------------------------------------------------------------------- /manifests/components/high-availability/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | resources: 4 | - pdb.yaml 5 | patchesStrategicMerge: 6 | - patch.yaml 7 | -------------------------------------------------------------------------------- /manifests/components/high-availability/patch.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: metrics-server 6 | namespace: kube-system 7 | spec: 8 | replicas: 2 9 | strategy: 10 | rollingUpdate: 11 | maxUnavailable: 1 12 | template: 13 | spec: 14 | affinity: 15 | podAntiAffinity: 16 | requiredDuringSchedulingIgnoredDuringExecution: 17 | - labelSelector: 18 | matchLabels: 19 | k8s-app: metrics-server 20 | namespaces: 21 | - kube-system 22 | topologyKey: kubernetes.io/hostname 23 | -------------------------------------------------------------------------------- /manifests/components/high-availability/pdb.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: policy/v1beta1 3 | kind: PodDisruptionBudget 4 | metadata: 5 | name: metrics-server 6 | namespace: kube-system 7 | labels: 8 | k8s-app: metrics-server 9 | spec: 10 | minAvailable: 1 11 | selector: 12 | matchLabels: 13 | k8s-app: metrics-server 14 | -------------------------------------------------------------------------------- /manifests/components/release/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | images: 4 | - name: gcr.io/k8s-staging-metrics-server/metrics-server 5 | newName: registry.k8s.io/metrics-server/metrics-server 6 | newTag: v0.7.0 7 | -------------------------------------------------------------------------------- /manifests/components/test/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1alpha1 2 | kind: Component 3 | patchesJson6902: 4 | - target: 5 | group: apps 6 | version: v1 7 | kind: Deployment 8 | name: metrics-server 9 | namespace: kube-system 10 | patch: | 11 | - op: add 12 | path: /spec/template/spec/containers/0/args/- 13 | value: --kubelet-insecure-tls 14 | - op: add 15 | path: /spec/template/spec/containers/0/args/- 16 | value: --v=4 17 | - op: add 18 | path: /spec/template/spec/containers/0/imagePullPolicy 19 | value: Never 20 | - op: add 21 | path: /spec/template/spec/containers/0/args/- 22 | value: --node-selector=metrics-server-skip!=true 23 | -------------------------------------------------------------------------------- /manifests/overlays/release-ha-1.21+/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - ../../base 5 | components: 6 | - ../../components/high-availability-1.21+ 7 | - ../../components/release 8 | -------------------------------------------------------------------------------- /manifests/overlays/release-ha/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - ../../base 5 | components: 6 | - ../../components/high-availability 7 | - ../../components/release 8 | -------------------------------------------------------------------------------- /manifests/overlays/release/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - ../../base 5 | components: 6 | - ../../components/release 7 | -------------------------------------------------------------------------------- /manifests/overlays/test-ha/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - ../../base 5 | components: 6 | - ../../components/high-availability-1.21+ 7 | - ../../components/test 8 | -------------------------------------------------------------------------------- /manifests/overlays/test/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - ../../base 5 | components: 6 | - ../../components/test 7 | -------------------------------------------------------------------------------- /pkg/api/filter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package api 16 | 17 | import ( 18 | v1 "k8s.io/api/core/v1" 19 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 | "k8s.io/apimachinery/pkg/fields" 21 | "k8s.io/apimachinery/pkg/runtime" 22 | "k8s.io/apiserver/pkg/registry/generic" 23 | ) 24 | 25 | func filterNodes(nodes []*v1.Node, selector fields.Selector) []*v1.Node { 26 | newNodes := make([]*v1.Node, 0, len(nodes)) 27 | fields := make(fields.Set, 2) 28 | for _, node := range nodes { 29 | for k := range fields { 30 | delete(fields, k) 31 | } 32 | fieldsSet := generic.AddObjectMetaFieldsSet(fields, &node.ObjectMeta, false) 33 | if !selector.Matches(fieldsSet) { 34 | continue 35 | } 36 | newNodes = append(newNodes, node) 37 | } 38 | return newNodes 39 | } 40 | 41 | func filterPartialObjectMetadata(objs []runtime.Object, selector fields.Selector) []runtime.Object { 42 | newObjs := make([]runtime.Object, 0, len(objs)) 43 | fields := make(fields.Set, 2) 44 | for _, obj := range objs { 45 | for k := range fields { 46 | delete(fields, k) 47 | } 48 | fieldsSet := generic.AddObjectMetaFieldsSet(fields, &obj.(*metav1.PartialObjectMetadata).ObjectMeta, true) 49 | if !selector.Matches(fieldsSet) { 50 | continue 51 | } 52 | newObjs = append(newObjs, obj) 53 | } 54 | return newObjs 55 | } 56 | -------------------------------------------------------------------------------- /pkg/api/generated/openapi/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // package openapi contains autogenerated openapi schema definitions 16 | // for the metrics.k8s.io API. 17 | package openapi 18 | -------------------------------------------------------------------------------- /pkg/api/install.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package api 16 | 17 | import ( 18 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 | "k8s.io/apimachinery/pkg/labels" 20 | "k8s.io/apimachinery/pkg/runtime" 21 | "k8s.io/apimachinery/pkg/runtime/schema" 22 | "k8s.io/apimachinery/pkg/runtime/serializer" 23 | "k8s.io/apiserver/pkg/registry/rest" 24 | genericapiserver "k8s.io/apiserver/pkg/server" 25 | corev1 "k8s.io/client-go/listers/core/v1" 26 | "k8s.io/client-go/tools/cache" 27 | "k8s.io/metrics/pkg/apis/metrics" 28 | "k8s.io/metrics/pkg/apis/metrics/install" 29 | "k8s.io/metrics/pkg/apis/metrics/v1beta1" 30 | ) 31 | 32 | var ( 33 | // Scheme contains the types needed by the resource metrics API. 34 | Scheme = runtime.NewScheme() 35 | // Codecs is a codec factory for serving the resource metrics API. 36 | Codecs = serializer.NewCodecFactory(Scheme) 37 | ) 38 | 39 | func init() { 40 | install.Install(Scheme) 41 | metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) 42 | } 43 | 44 | // Build constructs APIGroupInfo the metrics.k8s.io API group using the given getters. 45 | func Build(pod, node rest.Storage) genericapiserver.APIGroupInfo { 46 | apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(metrics.GroupName, Scheme, metav1.ParameterCodec, Codecs) 47 | metricsServerResources := map[string]rest.Storage{ 48 | "nodes": node, 49 | "pods": pod, 50 | } 51 | apiGroupInfo.VersionedResourcesStorageMap[v1beta1.SchemeGroupVersion.Version] = metricsServerResources 52 | 53 | return apiGroupInfo 54 | } 55 | 56 | // Install builds the metrics for the metrics.k8s.io API, and then installs it into the given API metrics-server. 57 | func Install(m MetricsGetter, podMetadataLister cache.GenericLister, nodeLister corev1.NodeLister, server *genericapiserver.GenericAPIServer, nodeSelector []labels.Requirement) error { 58 | node := newNodeMetrics(metrics.Resource("nodemetrics"), m, nodeLister, nodeSelector) 59 | pod := newPodMetrics(metrics.Resource("podmetrics"), m, podMetadataLister) 60 | info := Build(pod, node) 61 | return server.InstallAPIGroup(&info) 62 | } 63 | -------------------------------------------------------------------------------- /pkg/api/interfaces.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package api 16 | 17 | import ( 18 | "time" 19 | 20 | corev1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/metrics/pkg/apis/metrics" 23 | ) 24 | 25 | // MetricsGetter is both a PodMetricsGetter and a NodeMetricsGetter 26 | type MetricsGetter interface { 27 | PodMetricsGetter 28 | NodeMetricsGetter 29 | } 30 | 31 | // TimeInfo represents the timing information for a metric, which was 32 | // potentially calculated over some window of time (e.g. for CPU usage rate). 33 | type TimeInfo struct { 34 | // NB: we consider the earliest timestamp amongst multiple containers 35 | // for the purposes of determining if a metric is tained by a time 36 | // period, like pod startup (used by things like the HPA). 37 | 38 | // Timestamp is the time at which the metrics were initially collected. 39 | // In the case of a rate metric, it should be the timestamp of the last 40 | // data point used in the calculation. If it represents multiple metric 41 | // points, it should be the earliest such timestamp from all of the points. 42 | Timestamp time.Time 43 | 44 | // Window represents the window used to calculate rate metrics associated 45 | // with this timestamp. 46 | Window time.Duration 47 | } 48 | 49 | // PodMetricsGetter knows how to fetch metrics for the containers in a pod. 50 | type PodMetricsGetter interface { 51 | // GetPodMetrics gets the latest metrics for all containers in each listed pod, 52 | // returning both the metrics and the associated collection timestamp. 53 | GetPodMetrics(pods ...*metav1.PartialObjectMetadata) ([]metrics.PodMetrics, error) 54 | } 55 | 56 | // NodeMetricsGetter knows how to fetch metrics for a node. 57 | type NodeMetricsGetter interface { 58 | // GetNodeMetrics gets the latest metrics for the given nodes, 59 | // returning both the metrics and the associated collection timestamp. 60 | GetNodeMetrics(nodes ...*corev1.Node) ([]metrics.NodeMetrics, error) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/api/monitoring.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package api 16 | 17 | import ( 18 | "k8s.io/component-base/metrics" 19 | ) 20 | 21 | var ( 22 | metricFreshness = metrics.NewHistogramVec( 23 | &metrics.HistogramOpts{ 24 | Namespace: "metrics_server", 25 | Subsystem: "api", 26 | Name: "metric_freshness_seconds", 27 | Help: "Freshness of metrics exported", 28 | Buckets: metrics.ExponentialBuckets(1, 1.364, 20), 29 | }, 30 | []string{}, 31 | ) 32 | ) 33 | 34 | // RegisterAPIMetrics registers a histogram metric for the freshness of 35 | // exported metrics. 36 | func RegisterAPIMetrics(registrationFunc func(metrics.Registerable) error) error { 37 | return registrationFunc(metricFreshness) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/api/node.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package api 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "sort" 21 | 22 | corev1 "k8s.io/api/core/v1" 23 | "k8s.io/apimachinery/pkg/api/errors" 24 | metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" 27 | "k8s.io/apimachinery/pkg/labels" 28 | "k8s.io/apimachinery/pkg/runtime" 29 | "k8s.io/apimachinery/pkg/runtime/schema" 30 | "k8s.io/apiserver/pkg/registry/rest" 31 | v1listers "k8s.io/client-go/listers/core/v1" 32 | "k8s.io/klog/v2" 33 | "k8s.io/metrics/pkg/apis/metrics" 34 | _ "k8s.io/metrics/pkg/apis/metrics/install" 35 | ) 36 | 37 | type nodeMetrics struct { 38 | groupResource schema.GroupResource 39 | metrics NodeMetricsGetter 40 | nodeLister v1listers.NodeLister 41 | nodeSelector []labels.Requirement 42 | } 43 | 44 | var _ rest.KindProvider = &nodeMetrics{} 45 | var _ rest.Storage = &nodeMetrics{} 46 | var _ rest.Getter = &nodeMetrics{} 47 | var _ rest.Lister = &nodeMetrics{} 48 | var _ rest.Scoper = &nodeMetrics{} 49 | var _ rest.TableConvertor = &nodeMetrics{} 50 | var _ rest.SingularNameProvider = &nodeMetrics{} 51 | 52 | func newNodeMetrics(groupResource schema.GroupResource, metrics NodeMetricsGetter, nodeLister v1listers.NodeLister, nodeSelector []labels.Requirement) *nodeMetrics { 53 | return &nodeMetrics{ 54 | groupResource: groupResource, 55 | metrics: metrics, 56 | nodeLister: nodeLister, 57 | nodeSelector: nodeSelector, 58 | } 59 | } 60 | 61 | // New implements rest.Storage interface 62 | func (m *nodeMetrics) New() runtime.Object { 63 | return &metrics.NodeMetrics{} 64 | } 65 | 66 | // Destroy implements rest.Storage interface 67 | func (m *nodeMetrics) Destroy() { 68 | } 69 | 70 | // Kind implements rest.KindProvider interface 71 | func (m *nodeMetrics) Kind() string { 72 | return "NodeMetrics" 73 | } 74 | 75 | // NewList implements rest.Lister interface 76 | func (m *nodeMetrics) NewList() runtime.Object { 77 | return &metrics.NodeMetricsList{} 78 | } 79 | 80 | // List implements rest.Lister interface 81 | func (m *nodeMetrics) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) { 82 | nodes, err := m.nodes(ctx, options) 83 | if err != nil { 84 | return &metrics.NodeMetricsList{}, err 85 | } 86 | 87 | ms, err := m.getMetrics(nodes...) 88 | if err != nil { 89 | klog.ErrorS(err, "Failed reading nodes metrics") 90 | return &metrics.NodeMetricsList{}, fmt.Errorf("failed reading nodes metrics: %w", err) 91 | } 92 | return &metrics.NodeMetricsList{Items: ms}, nil 93 | } 94 | 95 | func (m *nodeMetrics) nodes(ctx context.Context, options *metainternalversion.ListOptions) ([]*corev1.Node, error) { 96 | labelSelector := labels.Everything() 97 | if options != nil && options.LabelSelector != nil { 98 | labelSelector = options.LabelSelector 99 | } 100 | if m.nodeSelector != nil { 101 | labelSelector = labelSelector.Add(m.nodeSelector...) 102 | } 103 | nodes, err := m.nodeLister.List(labelSelector) 104 | if err != nil { 105 | klog.ErrorS(err, "Failed listing nodes", "labelSelector", labelSelector) 106 | return nil, fmt.Errorf("failed listing nodes: %w", err) 107 | } 108 | if options != nil && options.FieldSelector != nil { 109 | nodes = filterNodes(nodes, options.FieldSelector) 110 | } 111 | return nodes, nil 112 | } 113 | 114 | // Get implements rest.Getter interface 115 | func (m *nodeMetrics) Get(ctx context.Context, name string, opts *metav1.GetOptions) (runtime.Object, error) { 116 | node, err := m.nodeLister.Get(name) 117 | if err != nil { 118 | if errors.IsNotFound(err) { 119 | // return not-found errors directly 120 | return nil, err 121 | } 122 | klog.ErrorS(err, "Failed getting node", "node", klog.KRef("", name)) 123 | return nil, fmt.Errorf("failed getting node: %w", err) 124 | } 125 | if node == nil { 126 | return nil, errors.NewNotFound(m.groupResource, name) 127 | } 128 | ms, err := m.getMetrics(node) 129 | if err != nil { 130 | klog.ErrorS(err, "Failed reading node metrics", "node", klog.KRef("", name)) 131 | return nil, fmt.Errorf("failed reading node metrics: %w", err) 132 | } 133 | if len(ms) == 0 { 134 | return nil, errors.NewNotFound(m.groupResource, name) 135 | } 136 | return &ms[0], nil 137 | } 138 | 139 | // ConvertToTable implements rest.TableConvertor interface 140 | func (m *nodeMetrics) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1beta1.Table, error) { 141 | var table metav1beta1.Table 142 | 143 | switch t := object.(type) { 144 | case *metrics.NodeMetrics: 145 | table.ResourceVersion = t.ResourceVersion 146 | table.SelfLink = t.SelfLink //nolint:staticcheck // keep deprecated field to be backward compatible 147 | addNodeMetricsToTable(&table, *t) 148 | case *metrics.NodeMetricsList: 149 | table.ResourceVersion = t.ResourceVersion 150 | table.SelfLink = t.SelfLink //nolint:staticcheck // keep deprecated field to be backward compatible 151 | table.Continue = t.Continue 152 | addNodeMetricsToTable(&table, t.Items...) 153 | default: 154 | } 155 | 156 | return &table, nil 157 | } 158 | 159 | func (m *nodeMetrics) getMetrics(nodes ...*corev1.Node) ([]metrics.NodeMetrics, error) { 160 | ms, err := m.metrics.GetNodeMetrics(nodes...) 161 | if err != nil { 162 | return nil, err 163 | } 164 | for _, m := range ms { 165 | metricFreshness.WithLabelValues().Observe(myClock.Since(m.Timestamp.Time).Seconds()) 166 | } 167 | // maintain the same ordering invariant as the Kube API would over nodes 168 | sort.Slice(ms, func(i, j int) bool { 169 | return ms[i].Name < ms[j].Name 170 | }) 171 | return ms, nil 172 | } 173 | 174 | // NamespaceScoped implements rest.Scoper interface 175 | func (m *nodeMetrics) NamespaceScoped() bool { 176 | return false 177 | } 178 | 179 | // GetSingularName implements rest.SingularNameProvider interface 180 | func (m *nodeMetrics) GetSingularName() string { 181 | return "" 182 | } 183 | -------------------------------------------------------------------------------- /pkg/api/pod.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package api 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "sort" 21 | 22 | corev1 "k8s.io/api/core/v1" 23 | "k8s.io/apimachinery/pkg/api/errors" 24 | metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" 27 | "k8s.io/apimachinery/pkg/labels" 28 | "k8s.io/apimachinery/pkg/runtime" 29 | "k8s.io/apimachinery/pkg/runtime/schema" 30 | genericapirequest "k8s.io/apiserver/pkg/endpoints/request" 31 | "k8s.io/apiserver/pkg/registry/rest" 32 | "k8s.io/client-go/tools/cache" 33 | "k8s.io/klog/v2" 34 | "k8s.io/metrics/pkg/apis/metrics" 35 | _ "k8s.io/metrics/pkg/apis/metrics/install" 36 | ) 37 | 38 | type podMetrics struct { 39 | groupResource schema.GroupResource 40 | metrics PodMetricsGetter 41 | podLister cache.GenericLister 42 | } 43 | 44 | var _ rest.KindProvider = &podMetrics{} 45 | var _ rest.Storage = &podMetrics{} 46 | var _ rest.Getter = &podMetrics{} 47 | var _ rest.Lister = &podMetrics{} 48 | var _ rest.TableConvertor = &podMetrics{} 49 | var _ rest.Scoper = &podMetrics{} 50 | var _ rest.SingularNameProvider = &podMetrics{} 51 | 52 | func newPodMetrics(groupResource schema.GroupResource, metrics PodMetricsGetter, podLister cache.GenericLister) *podMetrics { 53 | return &podMetrics{ 54 | groupResource: groupResource, 55 | metrics: metrics, 56 | podLister: podLister, 57 | } 58 | } 59 | 60 | // New implements rest.Storage interface 61 | func (m *podMetrics) New() runtime.Object { 62 | return &metrics.PodMetrics{} 63 | } 64 | 65 | // Destroy implements rest.Storage interface 66 | func (m *podMetrics) Destroy() { 67 | } 68 | 69 | // Kind implements rest.KindProvider interface 70 | func (m *podMetrics) Kind() string { 71 | return "PodMetrics" 72 | } 73 | 74 | // NewList implements rest.Lister interface 75 | func (m *podMetrics) NewList() runtime.Object { 76 | return &metrics.PodMetricsList{} 77 | } 78 | 79 | // List implements rest.Lister interface 80 | func (m *podMetrics) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) { 81 | pods, err := m.pods(ctx, options) 82 | if err != nil { 83 | return &metrics.PodMetricsList{}, err 84 | } 85 | ms, err := m.getMetrics(pods...) 86 | if err != nil { 87 | namespace := genericapirequest.NamespaceValue(ctx) 88 | klog.ErrorS(err, "Failed reading pods metrics", "namespace", klog.KRef("", namespace)) 89 | return &metrics.PodMetricsList{}, fmt.Errorf("failed reading pods metrics: %w", err) 90 | } 91 | return &metrics.PodMetricsList{Items: ms}, nil 92 | } 93 | 94 | func (m *podMetrics) pods(ctx context.Context, options *metainternalversion.ListOptions) ([]runtime.Object, error) { 95 | labelSelector := labels.Everything() 96 | if options != nil && options.LabelSelector != nil { 97 | labelSelector = options.LabelSelector 98 | } 99 | 100 | namespace := genericapirequest.NamespaceValue(ctx) 101 | pods, err := m.podLister.ByNamespace(namespace).List(labelSelector) 102 | if err != nil { 103 | klog.ErrorS(err, "Failed listing pods", "labelSelector", labelSelector, "namespace", klog.KRef("", namespace)) 104 | return nil, fmt.Errorf("failed listing pods: %w", err) 105 | } 106 | if options != nil && options.FieldSelector != nil { 107 | pods = filterPartialObjectMetadata(pods, options.FieldSelector) 108 | } 109 | return pods, err 110 | } 111 | 112 | // Get implements rest.Getter interface 113 | func (m *podMetrics) Get(ctx context.Context, name string, opts *metav1.GetOptions) (runtime.Object, error) { 114 | namespace := genericapirequest.NamespaceValue(ctx) 115 | 116 | pod, err := m.podLister.ByNamespace(namespace).Get(name) 117 | if err != nil { 118 | if errors.IsNotFound(err) { 119 | // return not-found errors directly 120 | return &metrics.PodMetrics{}, err 121 | } 122 | klog.ErrorS(err, "Failed getting pod", "pod", klog.KRef(namespace, name)) 123 | return &metrics.PodMetrics{}, fmt.Errorf("failed getting pod: %w", err) 124 | } 125 | if pod == nil { 126 | return &metrics.PodMetrics{}, errors.NewNotFound(corev1.Resource("pods"), fmt.Sprintf("%s/%s", namespace, name)) 127 | } 128 | 129 | ms, err := m.getMetrics(pod) 130 | if err != nil { 131 | klog.ErrorS(err, "Failed reading pod metrics", "pod", klog.KRef(namespace, name)) 132 | return nil, fmt.Errorf("failed pod metrics: %w", err) 133 | } 134 | if len(ms) == 0 { 135 | return nil, errors.NewNotFound(m.groupResource, fmt.Sprintf("%s/%s", namespace, name)) 136 | } 137 | return &ms[0], nil 138 | } 139 | 140 | // ConvertToTable implements rest.TableConvertor interface 141 | func (m *podMetrics) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1beta1.Table, error) { 142 | var table metav1beta1.Table 143 | 144 | switch t := object.(type) { 145 | case *metrics.PodMetrics: 146 | table.ResourceVersion = t.ResourceVersion 147 | table.SelfLink = t.SelfLink //nolint:staticcheck // keep deprecated field to be backward compatible 148 | addPodMetricsToTable(&table, *t) 149 | case *metrics.PodMetricsList: 150 | table.ResourceVersion = t.ResourceVersion 151 | table.SelfLink = t.SelfLink //nolint:staticcheck // keep deprecated field to be backward compatible 152 | table.Continue = t.Continue 153 | addPodMetricsToTable(&table, t.Items...) 154 | default: 155 | } 156 | 157 | return &table, nil 158 | } 159 | 160 | func (m *podMetrics) getMetrics(pods ...runtime.Object) ([]metrics.PodMetrics, error) { 161 | objs := make([]*metav1.PartialObjectMetadata, len(pods)) 162 | for i, pod := range pods { 163 | objs[i] = pod.(*metav1.PartialObjectMetadata) 164 | } 165 | ms, err := m.metrics.GetPodMetrics(objs...) 166 | if err != nil { 167 | return nil, err 168 | } 169 | for _, m := range ms { 170 | metricFreshness.WithLabelValues().Observe(myClock.Since(m.Timestamp.Time).Seconds()) 171 | } 172 | sort.Slice(ms, func(i, j int) bool { 173 | if ms[i].Namespace != ms[j].Namespace { 174 | return ms[i].Namespace < ms[j].Namespace 175 | } 176 | return ms[i].Name < ms[j].Name 177 | }) 178 | return ms, nil 179 | } 180 | 181 | // NamespaceScoped implements rest.Scoper interface 182 | func (m *podMetrics) NamespaceScoped() bool { 183 | return true 184 | } 185 | 186 | // GetSingularName implements rest.SingularNameProvider interface 187 | func (m *podMetrics) GetSingularName() string { 188 | return "" 189 | } 190 | -------------------------------------------------------------------------------- /pkg/api/table.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package api 16 | 17 | import ( 18 | "sort" 19 | 20 | v1 "k8s.io/api/core/v1" 21 | metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/metrics/pkg/apis/metrics" 24 | ) 25 | 26 | func addPodMetricsToTable(table *metav1beta1.Table, pods ...metrics.PodMetrics) { 27 | usage := make(v1.ResourceList, 3) 28 | var names []string 29 | for i, pod := range pods { 30 | for k := range usage { 31 | delete(usage, k) 32 | } 33 | for _, container := range pod.Containers { 34 | for k, v := range container.Usage { 35 | u := usage[k] 36 | u.Add(v) 37 | usage[k] = u 38 | } 39 | } 40 | if names == nil { 41 | for k := range usage { 42 | names = append(names, string(k)) 43 | } 44 | sort.Strings(names) 45 | 46 | table.ColumnDefinitions = []metav1beta1.TableColumnDefinition{ 47 | {Name: "Name", Type: "string", Format: "name", Description: "Name of the resource"}, 48 | } 49 | for _, name := range names { 50 | table.ColumnDefinitions = append(table.ColumnDefinitions, metav1beta1.TableColumnDefinition{ 51 | Name: name, 52 | Type: "string", 53 | Format: "quantity", 54 | }) 55 | } 56 | table.ColumnDefinitions = append(table.ColumnDefinitions, metav1beta1.TableColumnDefinition{ 57 | Name: "Window", 58 | Type: "string", 59 | Format: "duration", 60 | }) 61 | } 62 | row := make([]interface{}, 0, len(names)+1) 63 | row = append(row, pod.Name) 64 | for _, name := range names { 65 | v := usage[v1.ResourceName(name)] 66 | row = append(row, v.String()) 67 | } 68 | row = append(row, pod.Window.Duration.String()) 69 | table.Rows = append(table.Rows, metav1beta1.TableRow{ 70 | Cells: row, 71 | Object: runtime.RawExtension{Object: &pods[i]}, 72 | }) 73 | } 74 | } 75 | 76 | func addNodeMetricsToTable(table *metav1beta1.Table, nodes ...metrics.NodeMetrics) { 77 | var names []string 78 | for i, node := range nodes { 79 | if names == nil { 80 | for k := range node.Usage { 81 | names = append(names, string(k)) 82 | } 83 | sort.Strings(names) 84 | 85 | table.ColumnDefinitions = []metav1beta1.TableColumnDefinition{ 86 | {Name: "Name", Type: "string", Format: "name", Description: "Name of the resource"}, 87 | } 88 | for _, name := range names { 89 | table.ColumnDefinitions = append(table.ColumnDefinitions, metav1beta1.TableColumnDefinition{ 90 | Name: name, 91 | Type: "string", 92 | Format: "quantity", 93 | }) 94 | } 95 | table.ColumnDefinitions = append(table.ColumnDefinitions, metav1beta1.TableColumnDefinition{ 96 | Name: "Window", 97 | Type: "string", 98 | Format: "duration", 99 | }) 100 | } 101 | row := make([]interface{}, 0, len(names)+1) 102 | row = append(row, node.Name) 103 | for _, name := range names { 104 | v := node.Usage[v1.ResourceName(name)] 105 | row = append(row, v.String()) 106 | } 107 | row = append(row, node.Window.Duration.String()) 108 | table.Rows = append(table.Rows, metav1beta1.TableRow{ 109 | Cells: row, 110 | Object: runtime.RawExtension{Object: &nodes[i]}, 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /pkg/api/table_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 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 | 17 | package api 18 | 19 | import ( 20 | "testing" 21 | 22 | genericapirequest "k8s.io/apiserver/pkg/endpoints/request" 23 | ) 24 | 25 | func TestNodeList_ConvertToTable(t *testing.T) { 26 | // setup 27 | r := NewTestNodeStorage(nil) 28 | 29 | // execute 30 | got, err := r.List(genericapirequest.NewContext(), nil) 31 | 32 | // assert 33 | if err != nil { 34 | t.Fatalf("Unexpected error: %v", err) 35 | } 36 | 37 | res, err := r.ConvertToTable(genericapirequest.NewContext(), got, nil) 38 | if err != nil { 39 | t.Fatalf("Unexpected error: %v", err) 40 | } 41 | 42 | if len(res.Rows) != 3 || 43 | res.ColumnDefinitions[1].Name != "res1" || res.ColumnDefinitions[2].Name != "Window" || 44 | res.Rows[0].Cells[0] != "node1" || 45 | res.Rows[0].Cells[1] != "10m" || 46 | res.Rows[0].Cells[2] != "1µs" || 47 | res.Rows[1].Cells[0] != "node2" || 48 | res.Rows[1].Cells[1] != "5Mi" || 49 | res.Rows[1].Cells[2] != "2µs" || 50 | res.Rows[2].Cells[0] != "node3" || 51 | res.Rows[2].Cells[1] != "1" || 52 | res.Rows[2].Cells[2] != "3µs" { 53 | t.Errorf("Got unexpected object: %+v", res) 54 | } 55 | } 56 | 57 | func TestPodList_ConvertToTable(t *testing.T) { 58 | // setup 59 | r := NewPodTestStorage(nil) 60 | 61 | // execute 62 | got, err := r.List(genericapirequest.NewContext(), nil) 63 | 64 | // assert 65 | if err != nil { 66 | t.Fatalf("Unexpected error: %v", err) 67 | } 68 | 69 | res, err := r.ConvertToTable(genericapirequest.NewContext(), got, nil) 70 | if err != nil { 71 | t.Fatalf("Unexpected error: %v", err) 72 | } 73 | 74 | if len(res.Rows) != 3 || 75 | res.ColumnDefinitions[1].Name != "cpu" || res.ColumnDefinitions[2].Name != "memory" || res.ColumnDefinitions[3].Name != "Window" || 76 | res.Rows[0].Cells[0] != "pod1" || 77 | res.Rows[0].Cells[1] != "10m" || 78 | res.Rows[0].Cells[2] != "5Mi" || 79 | res.Rows[0].Cells[3] != "1µs" || 80 | res.Rows[1].Cells[0] != "pod2" || 81 | res.Rows[1].Cells[1] != "20m" || 82 | res.Rows[1].Cells[2] != "15Mi" || 83 | res.Rows[1].Cells[3] != "2µs" || 84 | res.Rows[2].Cells[0] != "pod3" || 85 | res.Rows[2].Cells[1] != "20m" || 86 | res.Rows[2].Cells[2] != "25Mi" || 87 | res.Rows[2].Cells[3] != "3µs" { 88 | t.Errorf("Got unexpected object: %+v", res) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /pkg/api/time.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 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 | package api 17 | 18 | import ( 19 | "time" 20 | ) 21 | 22 | var myClock clock = &realClock{} 23 | 24 | type clock interface { 25 | Now() time.Time 26 | Since(time.Time) time.Duration 27 | } 28 | 29 | type realClock struct{} 30 | 31 | func (realClock) Now() time.Time { return time.Now() } 32 | func (realClock) Since(d time.Time) time.Duration { return time.Since(d) } 33 | 34 | type fakeClock struct { 35 | now time.Time 36 | } 37 | 38 | func (c fakeClock) Now() time.Time { return c.now } 39 | func (c fakeClock) Since(d time.Time) time.Duration { return c.now.Sub(d) } 40 | -------------------------------------------------------------------------------- /pkg/scraper/client/configs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package client 16 | 17 | import ( 18 | corev1 "k8s.io/api/core/v1" 19 | "k8s.io/client-go/rest" 20 | ) 21 | 22 | // KubeletClientConfig represents configuration for connecting to Kubelets. 23 | type KubeletClientConfig struct { 24 | Client rest.Config 25 | AddressTypePriority []corev1.NodeAddressType 26 | Scheme string 27 | DefaultPort int 28 | UseNodeStatusPort bool 29 | } 30 | -------------------------------------------------------------------------------- /pkg/scraper/client/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package client 16 | 17 | import ( 18 | "context" 19 | 20 | v1 "k8s.io/api/core/v1" 21 | 22 | "sigs.k8s.io/metrics-server/pkg/storage" 23 | ) 24 | 25 | // KubeletMetricsGetter knows how to fetch metrics from the Kubelet 26 | type KubeletMetricsGetter interface { 27 | // GetMetrics fetches Resource metrics from the given Kubelet 28 | GetMetrics(ctx context.Context, node *v1.Node) (*storage.MetricsBatch, error) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/scraper/client/resource/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package resource 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "fmt" 21 | "io" 22 | "net" 23 | "net/http" 24 | "net/url" 25 | "strconv" 26 | "sync" 27 | "time" 28 | 29 | corev1 "k8s.io/api/core/v1" 30 | "k8s.io/client-go/rest" 31 | 32 | "sigs.k8s.io/metrics-server/pkg/scraper/client" 33 | "sigs.k8s.io/metrics-server/pkg/storage" 34 | "sigs.k8s.io/metrics-server/pkg/utils" 35 | ) 36 | 37 | const ( 38 | // AnnotationResourceMetricsPath is the annotation used to specify the path to the resource metrics endpoint. 39 | AnnotationResourceMetricsPath = "metrics.k8s.io/resource-metrics-path" 40 | ) 41 | 42 | type kubeletClient struct { 43 | defaultPort int 44 | useNodeStatusPort bool 45 | client *http.Client 46 | scheme string 47 | addrResolver utils.NodeAddressResolver 48 | buffers sync.Pool 49 | } 50 | 51 | var _ client.KubeletMetricsGetter = (*kubeletClient)(nil) 52 | 53 | func NewForConfig(config *client.KubeletClientConfig) (*kubeletClient, error) { 54 | transport, err := rest.TransportFor(&config.Client) 55 | if err != nil { 56 | return nil, fmt.Errorf("unable to construct transport: %v", err) 57 | } 58 | 59 | c := &http.Client{ 60 | Transport: transport, 61 | Timeout: config.Client.Timeout, 62 | } 63 | return newClient(c, utils.NewPriorityNodeAddressResolver(config.AddressTypePriority), config.DefaultPort, config.Scheme, config.UseNodeStatusPort), nil 64 | } 65 | 66 | func newClient(c *http.Client, resolver utils.NodeAddressResolver, defaultPort int, scheme string, useNodeStatusPort bool) *kubeletClient { 67 | return &kubeletClient{ 68 | addrResolver: resolver, 69 | defaultPort: defaultPort, 70 | client: c, 71 | scheme: scheme, 72 | useNodeStatusPort: useNodeStatusPort, 73 | buffers: sync.Pool{ 74 | New: func() interface{} { 75 | buf := make([]byte, 10e3) 76 | return &buf 77 | }, 78 | }, 79 | } 80 | } 81 | 82 | // GetMetrics implements client.KubeletMetricsGetter 83 | func (kc *kubeletClient) GetMetrics(ctx context.Context, node *corev1.Node) (*storage.MetricsBatch, error) { 84 | port := kc.defaultPort 85 | path := "/metrics/resource" 86 | nodeStatusPort := int(node.Status.DaemonEndpoints.KubeletEndpoint.Port) 87 | if kc.useNodeStatusPort && nodeStatusPort != 0 { 88 | port = nodeStatusPort 89 | } 90 | if metricsPath := node.Annotations[AnnotationResourceMetricsPath]; metricsPath != "" { 91 | path = metricsPath 92 | } 93 | addr, err := kc.addrResolver.NodeAddress(node) 94 | if err != nil { 95 | return nil, err 96 | } 97 | url := url.URL{ 98 | Scheme: kc.scheme, 99 | Host: net.JoinHostPort(addr, strconv.Itoa(port)), 100 | Path: path, 101 | } 102 | return kc.getMetrics(ctx, url.String(), node.Name) 103 | } 104 | 105 | func (kc *kubeletClient) getMetrics(ctx context.Context, url, nodeName string) (*storage.MetricsBatch, error) { 106 | req, err := http.NewRequest("GET", url, nil) 107 | if err != nil { 108 | return nil, err 109 | } 110 | requestTime := time.Now() 111 | response, err := kc.client.Do(req.WithContext(ctx)) 112 | if err != nil { 113 | return nil, err 114 | } 115 | defer response.Body.Close() 116 | if response.StatusCode != http.StatusOK { 117 | return nil, fmt.Errorf("request failed, status: %q", response.Status) 118 | } 119 | bp := kc.buffers.Get().(*[]byte) 120 | b := *bp 121 | defer func() { 122 | *bp = b 123 | kc.buffers.Put(bp) 124 | }() 125 | buf := bytes.NewBuffer(b) 126 | buf.Reset() 127 | _, err = io.Copy(buf, response.Body) 128 | if err != nil { 129 | return nil, fmt.Errorf("failed to read response body - %v", err) 130 | } 131 | b = buf.Bytes() 132 | ms, err := decodeBatch(b, requestTime, nodeName) 133 | if err != nil { 134 | return nil, err 135 | } 136 | return ms, nil 137 | } 138 | -------------------------------------------------------------------------------- /pkg/scraper/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package scraper 16 | 17 | import ( 18 | "context" 19 | 20 | "sigs.k8s.io/metrics-server/pkg/storage" 21 | ) 22 | 23 | type Scraper interface { 24 | Scrape(ctx context.Context) *storage.MetricsBatch 25 | } 26 | -------------------------------------------------------------------------------- /pkg/scraper/scraper.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package scraper 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "math/rand" 21 | "time" 22 | 23 | corev1 "k8s.io/api/core/v1" 24 | "k8s.io/apimachinery/pkg/labels" 25 | apitypes "k8s.io/apimachinery/pkg/types" 26 | v1listers "k8s.io/client-go/listers/core/v1" 27 | "k8s.io/component-base/metrics" 28 | "k8s.io/klog/v2" 29 | 30 | "sigs.k8s.io/metrics-server/pkg/scraper/client" 31 | "sigs.k8s.io/metrics-server/pkg/storage" 32 | ) 33 | 34 | const ( 35 | maxDelayMs = 4 * 1000 36 | delayPerSourceMs = 8 37 | ) 38 | 39 | var ( 40 | requestDuration = metrics.NewHistogramVec( 41 | &metrics.HistogramOpts{ 42 | Namespace: "metrics_server", 43 | Subsystem: "kubelet", 44 | Name: "request_duration_seconds", 45 | Help: "Duration of requests to Kubelet API in seconds", 46 | Buckets: metrics.DefBuckets, 47 | }, 48 | []string{"node"}, 49 | ) 50 | requestTotal = metrics.NewCounterVec( 51 | &metrics.CounterOpts{ 52 | Namespace: "metrics_server", 53 | Subsystem: "kubelet", 54 | Name: "request_total", 55 | Help: "Number of requests sent to Kubelet API", 56 | }, 57 | []string{"success"}, 58 | ) 59 | lastRequestTime = metrics.NewGaugeVec( 60 | &metrics.GaugeOpts{ 61 | Namespace: "metrics_server", 62 | Subsystem: "kubelet", 63 | Name: "last_request_time_seconds", 64 | Help: "Time of last request performed to Kubelet API since unix epoch in seconds", 65 | }, 66 | []string{"node"}, 67 | ) 68 | ) 69 | 70 | // RegisterScraperMetrics registers rate, errors, and duration metrics on 71 | // Kubelet API scrapes. 72 | func RegisterScraperMetrics(registrationFunc func(metrics.Registerable) error) error { 73 | for _, metric := range []metrics.Registerable{ 74 | requestDuration, 75 | requestTotal, 76 | lastRequestTime, 77 | } { 78 | err := registrationFunc(metric) 79 | if err != nil { 80 | return err 81 | } 82 | } 83 | return nil 84 | } 85 | 86 | func NewScraper(nodeLister v1listers.NodeLister, client client.KubeletMetricsGetter, scrapeTimeout time.Duration, labelRequirement []labels.Requirement) *scraper { 87 | labelSelector := labels.Everything() 88 | if labelRequirement != nil { 89 | labelSelector = labelSelector.Add(labelRequirement...) 90 | } 91 | return &scraper{ 92 | nodeLister: nodeLister, 93 | kubeletClient: client, 94 | scrapeTimeout: scrapeTimeout, 95 | labelSelector: labelSelector, 96 | } 97 | } 98 | 99 | type scraper struct { 100 | nodeLister v1listers.NodeLister 101 | kubeletClient client.KubeletMetricsGetter 102 | scrapeTimeout time.Duration 103 | labelSelector labels.Selector 104 | } 105 | 106 | var _ Scraper = (*scraper)(nil) 107 | 108 | // NodeInfo contains the information needed to identify and connect to a particular node 109 | // (node name and preferred address). 110 | type NodeInfo struct { 111 | Name string 112 | ConnectAddress string 113 | } 114 | 115 | func (c *scraper) Scrape(baseCtx context.Context) *storage.MetricsBatch { 116 | nodes, err := c.nodeLister.List(c.labelSelector) 117 | if err != nil { 118 | // report the error and continue on in case of partial results 119 | klog.ErrorS(err, "Failed to list nodes") 120 | } 121 | klog.V(1).InfoS("Scraping metrics from nodes", "nodes", klog.KObjSlice(nodes), "nodeCount", len(nodes), "nodeSelector", c.labelSelector) 122 | 123 | responseChannel := make(chan *storage.MetricsBatch, len(nodes)) 124 | defer close(responseChannel) 125 | 126 | startTime := myClock.Now() 127 | 128 | // TODO(serathius): re-evaluate this code -- do we really need to stagger fetches like this? 129 | delayMs := delayPerSourceMs * len(nodes) 130 | if delayMs > maxDelayMs { 131 | delayMs = maxDelayMs 132 | } 133 | 134 | for _, node := range nodes { 135 | go func(node *corev1.Node) { 136 | // Prevents network congestion. 137 | sleepDuration := time.Duration(rand.Intn(delayMs)) * time.Millisecond 138 | time.Sleep(sleepDuration) 139 | // make the timeout a bit shorter to account for staggering, so we still preserve 140 | // the overall timeout 141 | ctx, cancelTimeout := context.WithTimeout(baseCtx, c.scrapeTimeout) 142 | defer cancelTimeout() 143 | klog.V(2).InfoS("Scraping node", "node", klog.KObj(node)) 144 | m, err := c.collectNode(ctx, node) 145 | if err != nil { 146 | if errors.Is(err, context.DeadlineExceeded) { 147 | klog.ErrorS(err, "Failed to scrape node, timeout to access kubelet", "node", klog.KObj(node), "timeout", c.scrapeTimeout) 148 | } else { 149 | klog.ErrorS(err, "Failed to scrape node", "node", klog.KObj(node)) 150 | } 151 | } 152 | responseChannel <- m 153 | }(node) 154 | } 155 | 156 | res := &storage.MetricsBatch{ 157 | Nodes: map[string]storage.MetricsPoint{}, 158 | Pods: map[apitypes.NamespacedName]storage.PodMetricsPoint{}, 159 | } 160 | 161 | for range nodes { 162 | srcBatch := <-responseChannel 163 | if srcBatch == nil { 164 | continue 165 | } 166 | for nodeName, nodeMetricsPoint := range srcBatch.Nodes { 167 | if _, nodeFind := res.Nodes[nodeName]; nodeFind { 168 | klog.ErrorS(nil, "Got duplicate node point", "node", klog.KRef("", nodeName)) 169 | continue 170 | } 171 | res.Nodes[nodeName] = nodeMetricsPoint 172 | } 173 | for podRef, podMetricsPoint := range srcBatch.Pods { 174 | if _, podFind := res.Pods[podRef]; podFind { 175 | klog.ErrorS(nil, "Got duplicate pod point", "pod", klog.KRef(podRef.Namespace, podRef.Name)) 176 | continue 177 | } 178 | res.Pods[podRef] = podMetricsPoint 179 | } 180 | } 181 | 182 | klog.V(1).InfoS("Scrape finished", "duration", myClock.Since(startTime), "nodeCount", len(res.Nodes), "podCount", len(res.Pods)) 183 | return res 184 | } 185 | 186 | func (c *scraper) collectNode(ctx context.Context, node *corev1.Node) (*storage.MetricsBatch, error) { 187 | startTime := myClock.Now() 188 | defer func() { 189 | requestDuration.WithLabelValues(node.Name).Observe(float64(myClock.Since(startTime)) / float64(time.Second)) 190 | lastRequestTime.WithLabelValues(node.Name).Set(float64(myClock.Now().Unix())) 191 | }() 192 | ms, err := c.kubeletClient.GetMetrics(ctx, node) 193 | 194 | if err != nil { 195 | requestTotal.WithLabelValues("false").Inc() 196 | return nil, err 197 | } 198 | requestTotal.WithLabelValues("true").Inc() 199 | return ms, nil 200 | } 201 | 202 | type clock interface { 203 | Now() time.Time 204 | Since(time.Time) time.Duration 205 | } 206 | 207 | type realClock struct{} 208 | 209 | func (realClock) Now() time.Time { return time.Now() } 210 | func (realClock) Since(d time.Time) time.Duration { return time.Since(d) } 211 | 212 | var myClock clock = &realClock{} 213 | -------------------------------------------------------------------------------- /pkg/server/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package server 15 | 16 | import ( 17 | "fmt" 18 | "net/http" 19 | "strings" 20 | "time" 21 | 22 | corev1 "k8s.io/api/core/v1" 23 | "k8s.io/apimachinery/pkg/labels" 24 | apimetrics "k8s.io/apiserver/pkg/endpoints/metrics" 25 | genericapiserver "k8s.io/apiserver/pkg/server" 26 | "k8s.io/client-go/rest" 27 | "k8s.io/component-base/metrics" 28 | "k8s.io/component-base/metrics/legacyregistry" 29 | _ "k8s.io/component-base/metrics/prometheus/restclient" // for client-go metrics registration 30 | 31 | "sigs.k8s.io/metrics-server/pkg/api" 32 | "sigs.k8s.io/metrics-server/pkg/scraper" 33 | "sigs.k8s.io/metrics-server/pkg/scraper/client" 34 | "sigs.k8s.io/metrics-server/pkg/scraper/client/resource" 35 | "sigs.k8s.io/metrics-server/pkg/storage" 36 | ) 37 | 38 | type Config struct { 39 | Apiserver *genericapiserver.Config 40 | Rest *rest.Config 41 | Kubelet *client.KubeletClientConfig 42 | MetricResolution time.Duration 43 | ScrapeTimeout time.Duration 44 | NodeSelector string 45 | } 46 | 47 | func (c Config) Complete() (*server, error) { 48 | var labelRequirement []labels.Requirement 49 | 50 | podInformerFactory, err := runningPodMetadataInformer(c.Rest) 51 | if err != nil { 52 | return nil, err 53 | } 54 | podInformer := podInformerFactory.ForResource(corev1.SchemeGroupVersion.WithResource("pods")) 55 | informer, err := informerFactory(c.Rest) 56 | if err != nil { 57 | return nil, err 58 | } 59 | kubeletClient, err := resource.NewForConfig(c.Kubelet) 60 | if err != nil { 61 | return nil, fmt.Errorf("unable to construct a client to connect to the kubelets: %v", err) 62 | } 63 | nodes := informer.Core().V1().Nodes() 64 | ns := strings.TrimSpace(c.NodeSelector) 65 | if ns != "" { 66 | labelRequirement, err = labels.ParseToRequirements(ns) 67 | if err != nil { 68 | return nil, err 69 | } 70 | } 71 | scrape := scraper.NewScraper(nodes.Lister(), kubeletClient, c.ScrapeTimeout, labelRequirement) 72 | 73 | // Disable default metrics handler and create custom one 74 | c.Apiserver.EnableMetrics = false 75 | metricsHandler, err := c.metricsHandler() 76 | if err != nil { 77 | return nil, err 78 | } 79 | genericServer, err := c.Apiserver.Complete(nil).New("metrics-server", genericapiserver.NewEmptyDelegate()) 80 | if err != nil { 81 | return nil, err 82 | } 83 | genericServer.Handler.NonGoRestfulMux.HandleFunc("/metrics", metricsHandler) 84 | 85 | store := storage.NewStorage(c.MetricResolution) 86 | if err := api.Install(store, podInformer.Lister(), nodes.Lister(), genericServer, labelRequirement); err != nil { 87 | return nil, err 88 | } 89 | 90 | s := NewServer( 91 | nodes.Informer(), 92 | podInformer.Informer(), 93 | genericServer, 94 | store, 95 | scrape, 96 | c.MetricResolution, 97 | ) 98 | err = s.RegisterProbes(podInformerFactory) 99 | if err != nil { 100 | return nil, err 101 | } 102 | return s, nil 103 | } 104 | 105 | func (c Config) metricsHandler() (http.HandlerFunc, error) { 106 | // Create registry for Metrics Server metrics 107 | registry := metrics.NewKubeRegistry() 108 | err := RegisterMetrics(registry, c.MetricResolution) 109 | if err != nil { 110 | return nil, err 111 | } 112 | // Register apiserver metrics in legacy registry 113 | apimetrics.Register() 114 | 115 | // Return handler that serves metrics from both legacy and Metrics Server registry 116 | return func(w http.ResponseWriter, req *http.Request) { 117 | legacyregistry.Handler().ServeHTTP(w, req) 118 | metrics.HandlerFor(registry, metrics.HandlerOpts{}).ServeHTTP(w, req) 119 | }, nil 120 | } 121 | -------------------------------------------------------------------------------- /pkg/server/health.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package server 16 | 17 | import ( 18 | "fmt" 19 | "net/http" 20 | 21 | "k8s.io/apimachinery/pkg/runtime/schema" 22 | "k8s.io/apiserver/pkg/server/healthz" 23 | ) 24 | 25 | type cacheSyncWaiter interface { 26 | WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool 27 | } 28 | 29 | type metadataInformerSync struct { 30 | name string 31 | cacheSyncWaiter cacheSyncWaiter 32 | } 33 | 34 | var _ healthz.HealthChecker = &metadataInformerSync{} 35 | 36 | // MetadataInformerSyncHealthz returns a new HealthChecker that will pass only if all informers in the given cacheSyncWaiter sync. 37 | func MetadataInformerSyncHealthz(name string, cacheSyncWaiter cacheSyncWaiter) healthz.HealthChecker { 38 | return &metadataInformerSync{ 39 | name: name, 40 | cacheSyncWaiter: cacheSyncWaiter, 41 | } 42 | } 43 | 44 | func (i *metadataInformerSync) Name() string { 45 | return i.name 46 | } 47 | 48 | func (i *metadataInformerSync) Check(_ *http.Request) error { 49 | stopCh := make(chan struct{}) 50 | // Close stopCh to force checking if informers are synced now. 51 | close(stopCh) 52 | 53 | informersByStarted := make(map[bool][]string) 54 | for informerType, started := range i.cacheSyncWaiter.WaitForCacheSync(stopCh) { 55 | informersByStarted[started] = append(informersByStarted[started], informerType.String()) 56 | } 57 | 58 | if notStarted := informersByStarted[false]; len(notStarted) > 0 { 59 | return fmt.Errorf("%d informers not started yet: %v", len(notStarted), notStarted) 60 | } 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/server/informer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package server 16 | 17 | import ( 18 | "fmt" 19 | 20 | corev1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/client-go/informers" 23 | "k8s.io/client-go/kubernetes" 24 | "k8s.io/client-go/metadata" 25 | "k8s.io/client-go/metadata/metadatainformer" 26 | "k8s.io/client-go/rest" 27 | ) 28 | 29 | const ( 30 | // we should never need to resync, since we're not worried about missing events, 31 | // and resync is actually for regular interval-based reconciliation these days, 32 | // so set the default resync interval to 0 33 | defaultResync = 0 34 | ) 35 | 36 | func informerFactory(rest *rest.Config) (informers.SharedInformerFactory, error) { 37 | client, err := kubernetes.NewForConfig(rest) 38 | if err != nil { 39 | return nil, fmt.Errorf("unable to construct lister client: %v", err) 40 | } 41 | return informers.NewSharedInformerFactory(client, defaultResync), nil 42 | } 43 | 44 | func runningPodMetadataInformer(rest *rest.Config) (metadatainformer.SharedInformerFactory, error) { 45 | client, err := metadata.NewForConfig(rest) 46 | if err != nil { 47 | return nil, fmt.Errorf("unable to construct lister client: %v", err) 48 | } 49 | return metadatainformer.NewFilteredSharedInformerFactory(client, defaultResync, corev1.NamespaceAll, func(options *metav1.ListOptions) { 50 | options.FieldSelector = "status.phase=Running" 51 | }), nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/server/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package server 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "k8s.io/component-base/metrics" 22 | 23 | "sigs.k8s.io/metrics-server/pkg/api" 24 | "sigs.k8s.io/metrics-server/pkg/scraper" 25 | "sigs.k8s.io/metrics-server/pkg/storage" 26 | ) 27 | 28 | // RegisterMetrics registers 29 | func RegisterMetrics(r metrics.KubeRegistry, metricResolution time.Duration) error { 30 | // register metrics server components metrics 31 | err := RegisterServerMetrics(r.Register, metricResolution) 32 | if err != nil { 33 | return fmt.Errorf("unable to register server metrics: %v", err) 34 | } 35 | err = scraper.RegisterScraperMetrics(r.Register) 36 | if err != nil { 37 | return fmt.Errorf("unable to register scraper metrics: %v", err) 38 | } 39 | err = api.RegisterAPIMetrics(r.Register) 40 | if err != nil { 41 | return fmt.Errorf("unable to register API metrics: %v", err) 42 | } 43 | err = storage.RegisterStorageMetrics(r.Register) 44 | if err != nil { 45 | return fmt.Errorf("unable to register storage metrics: %v", err) 46 | } 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/server/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package server 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "net/http" 21 | "sync" 22 | "time" 23 | 24 | "k8s.io/apimachinery/pkg/util/wait" 25 | genericapiserver "k8s.io/apiserver/pkg/server" 26 | "k8s.io/apiserver/pkg/server/healthz" 27 | "k8s.io/client-go/tools/cache" 28 | "k8s.io/component-base/metrics" 29 | "k8s.io/klog/v2" 30 | 31 | "sigs.k8s.io/metrics-server/pkg/scraper" 32 | "sigs.k8s.io/metrics-server/pkg/storage" 33 | "sigs.k8s.io/metrics-server/pkg/utils" 34 | ) 35 | 36 | var ( 37 | // initialized below to an actual value by a call to RegisterTickDuration 38 | // (acts as a no-op by default), but we can't just register it in the constructor, 39 | // since it could be called multiple times during setup. 40 | tickDuration = metrics.NewHistogram(&metrics.HistogramOpts{}) 41 | ) 42 | 43 | // RegisterServerMetrics creates and registers a histogram metric for 44 | // scrape duration. 45 | func RegisterServerMetrics(registrationFunc func(metrics.Registerable) error, resolution time.Duration) error { 46 | tickDuration = metrics.NewHistogram( 47 | &metrics.HistogramOpts{ 48 | Namespace: "metrics_server", 49 | Subsystem: "manager", 50 | Name: "tick_duration_seconds", 51 | Help: "The total time spent collecting and storing metrics in seconds.", 52 | Buckets: utils.BucketsForScrapeDuration(resolution), 53 | }, 54 | ) 55 | return registrationFunc(tickDuration) 56 | } 57 | 58 | func NewServer( 59 | nodes cache.Controller, 60 | pods cache.Controller, 61 | apiserver *genericapiserver.GenericAPIServer, storage storage.Storage, 62 | scraper scraper.Scraper, resolution time.Duration) *server { 63 | return &server{ 64 | nodes: nodes, 65 | pods: pods, 66 | GenericAPIServer: apiserver, 67 | storage: storage, 68 | scraper: scraper, 69 | resolution: resolution, 70 | } 71 | } 72 | 73 | // server scrapes metrics and serves then using k8s api. 74 | type server struct { 75 | *genericapiserver.GenericAPIServer 76 | 77 | pods cache.Controller 78 | nodes cache.Controller 79 | 80 | storage storage.Storage 81 | scraper scraper.Scraper 82 | resolution time.Duration 83 | 84 | // tickStatusMux protects tick fields 85 | tickStatusMux sync.RWMutex 86 | // tickLastStart is equal to start time of last unfinished tick 87 | tickLastStart time.Time 88 | } 89 | 90 | // RunUntil starts background scraping goroutine and runs apiserver serving metrics. 91 | func (s *server) RunUntil(stopCh <-chan struct{}) error { 92 | ctx, cancel := context.WithCancel(context.Background()) 93 | defer cancel() 94 | 95 | // Start informers 96 | go s.nodes.Run(stopCh) 97 | go s.pods.Run(stopCh) 98 | 99 | // Ensure cache is up to date 100 | ok := cache.WaitForCacheSync(stopCh, s.nodes.HasSynced) 101 | if !ok { 102 | return nil 103 | } 104 | ok = cache.WaitForCacheSync(stopCh, s.pods.HasSynced) 105 | if !ok { 106 | return nil 107 | } 108 | 109 | // Start serving API and scrape loop 110 | go s.runScrape(ctx) 111 | return s.GenericAPIServer.PrepareRun().RunWithContext(wait.ContextForChannel(stopCh)) 112 | } 113 | 114 | func (s *server) runScrape(ctx context.Context) { 115 | ticker := time.NewTicker(s.resolution) 116 | defer ticker.Stop() 117 | s.tick(ctx, time.Now()) 118 | 119 | for { 120 | select { 121 | case startTime := <-ticker.C: 122 | s.tick(ctx, startTime) 123 | case <-ctx.Done(): 124 | return 125 | } 126 | } 127 | } 128 | 129 | func (s *server) tick(ctx context.Context, startTime time.Time) { 130 | s.tickStatusMux.Lock() 131 | s.tickLastStart = startTime 132 | s.tickStatusMux.Unlock() 133 | 134 | ctx, cancelTimeout := context.WithTimeout(ctx, s.resolution) 135 | defer cancelTimeout() 136 | 137 | klog.V(6).InfoS("Scraping metrics") 138 | data := s.scraper.Scrape(ctx) 139 | 140 | klog.V(6).InfoS("Storing metrics") 141 | s.storage.Store(data) 142 | 143 | collectTime := time.Since(startTime) 144 | tickDuration.Observe(float64(collectTime) / float64(time.Second)) 145 | klog.V(6).InfoS("Scraping cycle complete") 146 | } 147 | 148 | func (s *server) RegisterProbes(waiter cacheSyncWaiter) error { 149 | err := s.AddReadyzChecks(s.probeMetricStorageReady("metric-storage-ready")) 150 | if err != nil { 151 | return err 152 | } 153 | err = s.AddReadyzChecks(s.probeMetricCacheHasSynced("metric-informer-sync")) 154 | if err != nil { 155 | return err 156 | } 157 | err = s.AddLivezChecks(0, s.probeMetricCollectionTimely("metric-collection-timely")) 158 | if err != nil { 159 | return err 160 | } 161 | err = s.AddHealthChecks(MetadataInformerSyncHealthz("metadata-informer-sync", waiter)) 162 | if err != nil { 163 | return err 164 | } 165 | return nil 166 | } 167 | 168 | // Check if MS is alive by looking at last tick time. 169 | // If its deadlock or panic, tick wouldn't be happening on the tick interval 170 | func (s *server) probeMetricCollectionTimely(name string) healthz.HealthChecker { 171 | return healthz.NamedCheck(name, func(_ *http.Request) error { 172 | s.tickStatusMux.RLock() 173 | tickLastStart := s.tickLastStart 174 | s.tickStatusMux.RUnlock() 175 | 176 | maxTickWait := time.Duration(1.5 * float64(s.resolution)) 177 | tickWait := time.Since(tickLastStart) 178 | if !tickLastStart.IsZero() && tickWait > maxTickWait { 179 | err := fmt.Errorf("metric collection didn't finish on time") 180 | klog.InfoS("Failed probe", "probe", name, "err", err, "duration", tickWait, "maxDuration", maxTickWait) 181 | return err 182 | } 183 | return nil 184 | }) 185 | } 186 | 187 | // Check if MS is ready by checking if last tick was ok 188 | func (s *server) probeMetricStorageReady(name string) healthz.HealthChecker { 189 | return healthz.NamedCheck(name, func(r *http.Request) error { 190 | if !s.storage.Ready() { 191 | err := fmt.Errorf("no metrics to serve") 192 | klog.InfoS("Failed probe", "probe", name, "err", err) 193 | return err 194 | } 195 | return nil 196 | }) 197 | } 198 | 199 | // Check if MS is ready by checking if cache has synced 200 | func (s *server) probeMetricCacheHasSynced(name string) healthz.HealthChecker { 201 | return healthz.NamedCheck(name, func(r *http.Request) error { 202 | if !s.nodes.HasSynced() { 203 | err := fmt.Errorf("cache for node informer has not synced") 204 | klog.InfoS("Failed probe", "probe", name, "err", err) 205 | return err 206 | } 207 | if !s.pods.HasSynced() { 208 | err := fmt.Errorf("cache for pod informer has not synced") 209 | klog.InfoS("Failed probe", "probe", name, "err", err) 210 | return err 211 | } 212 | return nil 213 | }) 214 | } 215 | -------------------------------------------------------------------------------- /pkg/server/server_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package server 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "testing" 21 | "time" 22 | 23 | . "github.com/onsi/ginkgo/v2" 24 | . "github.com/onsi/gomega" 25 | 26 | corev1 "k8s.io/api/core/v1" 27 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | "k8s.io/metrics/pkg/apis/metrics" 29 | 30 | "sigs.k8s.io/metrics-server/pkg/scraper" 31 | "sigs.k8s.io/metrics-server/pkg/storage" 32 | ) 33 | 34 | func TestServer(t *testing.T) { 35 | RegisterFailHandler(Fail) 36 | RunSpecs(t, "Server Suite") 37 | } 38 | 39 | var _ = Describe("Server", func() { 40 | var ( 41 | resolution time.Duration 42 | server *server 43 | scraper *scraperMock 44 | store *storageMock 45 | ) 46 | 47 | BeforeEach(func() { 48 | resolution = 60 * time.Second 49 | scraper = &scraperMock{ 50 | result: &storage.MetricsBatch{ 51 | Nodes: map[string]storage.MetricsPoint{ 52 | "node1": { 53 | Timestamp: time.Now(), 54 | CumulativeCpuUsed: 0, 55 | MemoryUsage: 0, 56 | }, 57 | }, 58 | }, 59 | } 60 | store = &storageMock{} 61 | server = NewServer(nil, nil, nil, store, scraper, resolution) 62 | }) 63 | 64 | It("metric-collection-timely probe should pass before first scrape tick finishes", func() { 65 | check := server.probeMetricCollectionTimely("") 66 | Expect(check.Check(nil)).To(Succeed()) 67 | }) 68 | It("metric-collection-timely probe should pass if scrape fails", func() { 69 | scraper.err = fmt.Errorf("failed to scrape") 70 | server.tick(context.Background(), time.Now()) 71 | check := server.probeMetricCollectionTimely("") 72 | Expect(check.Check(nil)).To(Succeed()) 73 | }) 74 | It("metric-collection-timely probe should pass if scrape succeeds", func() { 75 | server.tick(context.Background(), time.Now().Add(-resolution)) 76 | check := server.probeMetricCollectionTimely("") 77 | Expect(check.Check(nil)).To(Succeed()) 78 | }) 79 | It("metric-collection-timely probe should fail if last scrape took longer than expected", func() { 80 | server.tick(context.Background(), time.Now().Add(-2*resolution)) 81 | check := server.probeMetricCollectionTimely("") 82 | Expect(check.Check(nil)).NotTo(Succeed()) 83 | }) 84 | It("metric-storage-ready probe should fail if store is not ready", func() { 85 | check := server.probeMetricStorageReady("") 86 | Expect(check.Check(nil)).NotTo(Succeed()) 87 | }) 88 | It("metric-storage-ready probe should pass if store is ready", func() { 89 | store.ready = true 90 | check := server.probeMetricStorageReady("") 91 | Expect(check.Check(nil)).To(Succeed()) 92 | }) 93 | }) 94 | 95 | type scraperMock struct { 96 | result *storage.MetricsBatch 97 | err error 98 | } 99 | 100 | var _ scraper.Scraper = (*scraperMock)(nil) 101 | 102 | func (s *scraperMock) Scrape(ctx context.Context) *storage.MetricsBatch { 103 | return s.result 104 | } 105 | 106 | type storageMock struct { 107 | ready bool 108 | } 109 | 110 | var _ storage.Storage = (*storageMock)(nil) 111 | 112 | func (s *storageMock) Store(batch *storage.MetricsBatch) {} 113 | 114 | func (s *storageMock) GetPodMetrics(pods ...*metav1.PartialObjectMetadata) ([]metrics.PodMetrics, error) { 115 | return nil, nil 116 | } 117 | 118 | func (s *storageMock) GetNodeMetrics(nodes ...*corev1.Node) ([]metrics.NodeMetrics, error) { 119 | return nil, nil 120 | } 121 | 122 | func (s *storageMock) Ready() bool { 123 | return s.ready 124 | } 125 | -------------------------------------------------------------------------------- /pkg/storage/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package storage 16 | 17 | import "sigs.k8s.io/metrics-server/pkg/api" 18 | 19 | type Storage interface { 20 | api.MetricsGetter 21 | Store(batch *MetricsBatch) 22 | Ready() bool 23 | } 24 | -------------------------------------------------------------------------------- /pkg/storage/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package storage 16 | 17 | import ( 18 | "testing" 19 | 20 | . "github.com/onsi/ginkgo/v2" 21 | . "github.com/onsi/gomega" 22 | ) 23 | 24 | func TestStorage(t *testing.T) { 25 | RegisterFailHandler(Fail) 26 | RunSpecs(t, "storage suite") 27 | } 28 | -------------------------------------------------------------------------------- /pkg/storage/monitoring.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package storage 16 | 17 | import ( 18 | "k8s.io/component-base/metrics" 19 | ) 20 | 21 | var ( 22 | pointsStored = metrics.NewGaugeVec( 23 | &metrics.GaugeOpts{ 24 | Namespace: "metrics_server", 25 | Subsystem: "storage", 26 | Name: "points", 27 | Help: "Number of metrics points stored.", 28 | }, 29 | []string{"type"}, 30 | ) 31 | ) 32 | 33 | // RegisterStorageMetrics registers a gauge metric for the number of metrics 34 | // points stored. 35 | func RegisterStorageMetrics(registrationFunc func(metrics.Registerable) error) error { 36 | return registrationFunc(pointsStored) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/storage/node.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 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 | package storage 17 | 18 | import ( 19 | "time" 20 | 21 | corev1 "k8s.io/api/core/v1" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "k8s.io/klog/v2" 24 | "k8s.io/metrics/pkg/apis/metrics" 25 | ) 26 | 27 | // nodeStorage stores last two node metric batches and calculates cpu & memory usage 28 | // 29 | // This implementation only stores metric points if they are newer than the 30 | // points already stored and the cpuUsageOverTime function used to handle 31 | // cumulative metrics assumes that the time window is different from 0. 32 | type nodeStorage struct { 33 | // last stores node metric points from last scrape 34 | last map[string]MetricsPoint 35 | // prev stores node metric points from scrape preceding the last one. 36 | // Points timestamp should proceed the corresponding points from last. 37 | prev map[string]MetricsPoint 38 | } 39 | 40 | func (s *nodeStorage) GetMetrics(nodes ...*corev1.Node) ([]metrics.NodeMetrics, error) { 41 | results := make([]metrics.NodeMetrics, 0, len(nodes)) 42 | for _, node := range nodes { 43 | last, found := s.last[node.Name] 44 | if !found { 45 | continue 46 | } 47 | 48 | prev, found := s.prev[node.Name] 49 | if !found { 50 | continue 51 | } 52 | rl, ti, err := resourceUsage(last, prev) 53 | if err != nil { 54 | klog.ErrorS(err, "Skipping node usage metric", "node", node) 55 | continue 56 | } 57 | results = append(results, metrics.NodeMetrics{ 58 | ObjectMeta: metav1.ObjectMeta{ 59 | Name: node.Name, 60 | Labels: node.Labels, 61 | CreationTimestamp: metav1.NewTime(time.Now()), 62 | }, 63 | Timestamp: metav1.NewTime(ti.Timestamp), 64 | Window: metav1.Duration{Duration: ti.Window}, 65 | Usage: rl, 66 | }) 67 | } 68 | return results, nil 69 | } 70 | 71 | func (s *nodeStorage) Store(batch *MetricsBatch) { 72 | lastNodes := make(map[string]MetricsPoint, len(batch.Nodes)) 73 | prevNodes := make(map[string]MetricsPoint, len(batch.Nodes)) 74 | for nodeName, newPoint := range batch.Nodes { 75 | if _, exists := lastNodes[nodeName]; exists { 76 | klog.ErrorS(nil, "Got duplicate node point", "node", klog.KRef("", nodeName)) 77 | continue 78 | } 79 | lastNodes[nodeName] = newPoint 80 | 81 | if lastNode, found := s.last[nodeName]; found { 82 | // If new point is different then one already stored 83 | if newPoint.Timestamp.After(lastNode.Timestamp) { 84 | // Move stored point to previous 85 | prevNodes[nodeName] = lastNode 86 | } else if prevPoint, found := s.prev[nodeName]; found { 87 | if prevPoint.Timestamp.Before(newPoint.Timestamp) { 88 | // Keep previous point 89 | prevNodes[nodeName] = prevPoint 90 | } else { 91 | klog.V(2).InfoS("Found new node metrics point is older than stored previous, drop previous", 92 | "node", nodeName, 93 | "previousTimestamp", prevPoint.Timestamp, 94 | "timestamp", newPoint.Timestamp) 95 | } 96 | } 97 | } 98 | } 99 | s.last = lastNodes 100 | s.prev = prevNodes 101 | 102 | // Only count last for which metrics can be returned. 103 | pointsStored.WithLabelValues("node").Set(float64(len(prevNodes))) 104 | } 105 | -------------------------------------------------------------------------------- /pkg/storage/pod.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 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 | package storage 17 | 18 | import ( 19 | "time" 20 | 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | apitypes "k8s.io/apimachinery/pkg/types" 23 | "k8s.io/klog/v2" 24 | "k8s.io/metrics/pkg/apis/metrics" 25 | 26 | "sigs.k8s.io/metrics-server/pkg/api" 27 | ) 28 | 29 | // fresh new container's minimum allowable time duration between start time and timestamp. 30 | // if time duration less than 10s, can produce inaccurate data 31 | const freshContainerMinMetricsResolution = 10 * time.Second 32 | 33 | // podStorage stores last two pod metric batches and calculates cpu & memory usage. 34 | // 35 | // This implementation only stores metric points if they are newer than the 36 | // points already stored and the cpuUsageOverTime function used to handle 37 | // cumulative metrics assumes that the time window is different from 0. 38 | type podStorage struct { 39 | // last stores pod metric points from last scrape 40 | last map[apitypes.NamespacedName]PodMetricsPoint 41 | // prev stores pod metric points from scrape preceding the last one. 42 | // Points timestamp should proceed the corresponding points from last and have same start time (no restart between them). 43 | prev map[apitypes.NamespacedName]PodMetricsPoint 44 | // scrape period of metrics server 45 | metricResolution time.Duration 46 | } 47 | 48 | func (s *podStorage) GetMetrics(pods ...*metav1.PartialObjectMetadata) ([]metrics.PodMetrics, error) { 49 | results := make([]metrics.PodMetrics, 0, len(pods)) 50 | for _, pod := range pods { 51 | lastPod, found := s.last[apitypes.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}] 52 | if !found { 53 | continue 54 | } 55 | 56 | prevPod, found := s.prev[apitypes.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}] 57 | if !found { 58 | continue 59 | } 60 | 61 | var ( 62 | cms = make([]metrics.ContainerMetrics, 0, len(lastPod.Containers)) 63 | earliestTimeInfo api.TimeInfo 64 | ) 65 | allContainersPresent := true 66 | for container, lastContainer := range lastPod.Containers { 67 | prevContainer, found := prevPod.Containers[container] 68 | if !found { 69 | allContainersPresent = false 70 | break 71 | } 72 | usage, ti, err := resourceUsage(lastContainer, prevContainer) 73 | if err != nil { 74 | klog.ErrorS(err, "Skipping container usage metric", "container", container, "pod", klog.KRef(pod.Namespace, pod.Name)) 75 | continue 76 | } 77 | cms = append(cms, metrics.ContainerMetrics{ 78 | Name: container, 79 | Usage: usage, 80 | }) 81 | if earliestTimeInfo.Timestamp.IsZero() || earliestTimeInfo.Timestamp.After(ti.Timestamp) { 82 | earliestTimeInfo = ti 83 | } 84 | } 85 | if allContainersPresent { 86 | results = append(results, metrics.PodMetrics{ 87 | ObjectMeta: metav1.ObjectMeta{ 88 | Name: pod.Name, 89 | Namespace: pod.Namespace, 90 | Labels: pod.Labels, 91 | CreationTimestamp: metav1.NewTime(time.Now()), 92 | }, 93 | Timestamp: metav1.NewTime(earliestTimeInfo.Timestamp), 94 | Window: metav1.Duration{Duration: earliestTimeInfo.Window}, 95 | Containers: cms, 96 | }) 97 | } 98 | } 99 | return results, nil 100 | } 101 | 102 | func (s *podStorage) Store(newPods *MetricsBatch) { 103 | lastPods := make(map[apitypes.NamespacedName]PodMetricsPoint, len(newPods.Pods)) 104 | prevPods := make(map[apitypes.NamespacedName]PodMetricsPoint, len(newPods.Pods)) 105 | var containerCount int 106 | for podRef, newPod := range newPods.Pods { 107 | podRef := apitypes.NamespacedName{Name: podRef.Name, Namespace: podRef.Namespace} 108 | if _, found := lastPods[podRef]; found { 109 | klog.ErrorS(nil, "Got duplicate pod point", "pod", klog.KRef(podRef.Namespace, podRef.Name)) 110 | continue 111 | } 112 | 113 | newLastPod := PodMetricsPoint{Containers: make(map[string]MetricsPoint, len(newPod.Containers))} 114 | newPrevPod := PodMetricsPoint{Containers: make(map[string]MetricsPoint, len(newPod.Containers))} 115 | for containerName, newPoint := range newPod.Containers { 116 | if _, exists := newLastPod.Containers[containerName]; exists { 117 | klog.ErrorS(nil, "Got duplicate Container point", "container", containerName, "pod", klog.KRef(podRef.Namespace, podRef.Name)) 118 | continue 119 | } 120 | newLastPod.Containers[containerName] = newPoint 121 | if newPoint.StartTime.Before(newPoint.Timestamp) && newPoint.Timestamp.Sub(newPoint.StartTime) < s.metricResolution && newPoint.Timestamp.Sub(newPoint.StartTime) >= freshContainerMinMetricsResolution { 122 | copied := newPoint 123 | copied.Timestamp = newPoint.StartTime 124 | copied.CumulativeCpuUsed = 0 125 | newPrevPod.Containers[containerName] = copied 126 | } else if lastPod, found := s.last[podRef]; found { 127 | // Keep previous metric point if newPoint has not restarted (new metric start time < stored timestamp) 128 | if lastContainer, found := lastPod.Containers[containerName]; found && newPoint.StartTime.Before(lastContainer.Timestamp) { 129 | // If new point is different then one already stored 130 | if newPoint.Timestamp.After(lastContainer.Timestamp) { 131 | // Move stored point to previous 132 | newPrevPod.Containers[containerName] = lastContainer 133 | } else if prevPod, found := s.prev[podRef]; found { 134 | if prevPod.Containers[containerName].Timestamp.Before(newPoint.Timestamp) { 135 | // Keep previous point 136 | newPrevPod.Containers[containerName] = prevPod.Containers[containerName] 137 | } else { 138 | klog.V(2).InfoS("Found new containerName metrics point is older than stored previous , drop previous", 139 | "containerName", containerName, 140 | "pod", klog.KRef(podRef.Namespace, podRef.Name), 141 | "previousTimestamp", prevPod.Containers[containerName].Timestamp, 142 | "timestamp", newPoint.Timestamp) 143 | } 144 | } 145 | } 146 | } 147 | } 148 | containerPoints := len(newPrevPod.Containers) 149 | if containerPoints > 0 { 150 | prevPods[podRef] = newPrevPod 151 | } 152 | lastPods[podRef] = newLastPod 153 | 154 | // Only count containers for which metrics can be returned. 155 | containerCount += containerPoints 156 | } 157 | s.last = lastPods 158 | s.prev = prevPods 159 | 160 | pointsStored.WithLabelValues("container").Set(float64(containerCount)) 161 | } 162 | -------------------------------------------------------------------------------- /pkg/storage/storage.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 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 | package storage 17 | 18 | import ( 19 | "sync" 20 | "time" 21 | 22 | corev1 "k8s.io/api/core/v1" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/metrics/pkg/apis/metrics" 25 | ) 26 | 27 | // nodeStorage is a thread save nodeStorage for node and pod metrics. 28 | type storage struct { 29 | mu sync.RWMutex 30 | pods podStorage 31 | nodes nodeStorage 32 | } 33 | 34 | var _ Storage = (*storage)(nil) 35 | 36 | func NewStorage(metricResolution time.Duration) *storage { 37 | return &storage{pods: podStorage{metricResolution: metricResolution}} 38 | } 39 | 40 | // Ready returns true if metrics-server's storage has accumulated enough metric 41 | // points to serve NodeMetrics. 42 | func (s *storage) Ready() bool { 43 | s.mu.RLock() 44 | defer s.mu.RUnlock() 45 | return len(s.nodes.prev) != 0 || len(s.pods.prev) != 0 46 | } 47 | 48 | func (s *storage) GetNodeMetrics(nodes ...*corev1.Node) ([]metrics.NodeMetrics, error) { 49 | s.mu.RLock() 50 | defer s.mu.RUnlock() 51 | return s.nodes.GetMetrics(nodes...) 52 | } 53 | 54 | func (s *storage) GetPodMetrics(pods ...*metav1.PartialObjectMetadata) ([]metrics.PodMetrics, error) { 55 | s.mu.RLock() 56 | defer s.mu.RUnlock() 57 | return s.pods.GetMetrics(pods...) 58 | } 59 | 60 | func (s *storage) Store(batch *MetricsBatch) { 61 | s.mu.Lock() 62 | defer s.mu.Unlock() 63 | s.nodes.Store(batch) 64 | s.pods.Store(batch) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/storage/storage_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package storage 16 | 17 | import ( 18 | "time" 19 | ) 20 | 21 | const ( 22 | MiByte = 1024 * 1024 23 | CoreSecond = 1000 * 1000 * 1000 24 | ) 25 | 26 | func newMetricsPoint(st time.Time, ts time.Time, cpu, memory uint64) MetricsPoint { 27 | return MetricsPoint{ 28 | StartTime: st, 29 | Timestamp: ts, 30 | CumulativeCpuUsed: cpu, 31 | MemoryUsage: memory, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/storage/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package storage 16 | 17 | import ( 18 | "fmt" 19 | "math" 20 | "time" 21 | 22 | corev1 "k8s.io/api/core/v1" 23 | "k8s.io/apimachinery/pkg/api/resource" 24 | apitypes "k8s.io/apimachinery/pkg/types" 25 | "k8s.io/klog/v2" 26 | 27 | "sigs.k8s.io/metrics-server/pkg/api" 28 | ) 29 | 30 | // MetricsBatch is a single batch of pod, container, and node metrics from some source. 31 | type MetricsBatch struct { 32 | Nodes map[string]MetricsPoint 33 | Pods map[apitypes.NamespacedName]PodMetricsPoint 34 | } 35 | 36 | // PodMetricsPoint contains the metrics for some pod's containers. 37 | type PodMetricsPoint struct { 38 | Containers map[string]MetricsPoint 39 | } 40 | 41 | // MetricsPoint represents the a set of specific metrics at some point in time. 42 | type MetricsPoint struct { 43 | // StartTime is the start time of container/node. Cumulative CPU usage at that moment should be equal zero. 44 | StartTime time.Time 45 | // Timestamp is the time when metric point was measured. If CPU and Memory was measured at different time it should equal CPU time to allow accurate CPU calculation. 46 | Timestamp time.Time 47 | // CumulativeCpuUsed is the cumulative cpu used at Timestamp from the StartTime of container/node. Unit: nano core * seconds. 48 | CumulativeCpuUsed uint64 49 | // MemoryUsage is the working set size. Unit: bytes. 50 | MemoryUsage uint64 51 | } 52 | 53 | func resourceUsage(last, prev MetricsPoint) (corev1.ResourceList, api.TimeInfo, error) { 54 | if last.StartTime.Before(prev.StartTime) { 55 | return corev1.ResourceList{}, api.TimeInfo{}, fmt.Errorf("unexpected decrease in startTime of node/container") 56 | } 57 | if last.CumulativeCpuUsed < prev.CumulativeCpuUsed { 58 | return corev1.ResourceList{}, api.TimeInfo{}, fmt.Errorf("unexpected decrease in cumulative CPU usage value") 59 | } 60 | window := last.Timestamp.Sub(prev.Timestamp) 61 | cpuUsage := float64(last.CumulativeCpuUsed-prev.CumulativeCpuUsed) / window.Seconds() 62 | return corev1.ResourceList{ 63 | corev1.ResourceCPU: uint64Quantity(uint64(cpuUsage), resource.DecimalSI, -9), 64 | corev1.ResourceMemory: uint64Quantity(last.MemoryUsage, resource.BinarySI, 0), 65 | }, api.TimeInfo{ 66 | Timestamp: last.Timestamp, 67 | Window: window, 68 | }, nil 69 | } 70 | 71 | // uint64Quantity converts a uint64 into a Quantity, which only has constructors 72 | // that work with int64 (except for parse, which requires costly round-trips to string). 73 | // We lose precision until we fit in an int64 if greater than the max int64 value. 74 | func uint64Quantity(val uint64, format resource.Format, scale resource.Scale) resource.Quantity { 75 | q := *resource.NewScaledQuantity(int64(val), scale) 76 | if val > math.MaxInt64 { 77 | // lose an decimal order-of-magnitude precision, 78 | // so we can fit into a scaled quantity 79 | klog.V(2).InfoS("Found unexpectedly large resource value, losing precision to fit in scaled resource.Quantity", "value", val) 80 | q = *resource.NewScaledQuantity(int64(val/10), resource.Scale(1)+scale) 81 | } 82 | q.Format = format 83 | return q 84 | } 85 | -------------------------------------------------------------------------------- /pkg/storage/types_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package storage 16 | 17 | import ( 18 | "math" 19 | "reflect" 20 | "testing" 21 | "time" 22 | 23 | "sigs.k8s.io/metrics-server/pkg/api" 24 | 25 | v1 "k8s.io/api/core/v1" 26 | "k8s.io/apimachinery/pkg/api/resource" 27 | ) 28 | 29 | func TestUint64Quantity(t *testing.T) { 30 | tcs := []struct { 31 | name string 32 | input uint64 33 | expect resource.Quantity 34 | }{ 35 | {"math.MaxInt64 + 10", uint64(math.MaxInt64 + 10), *resource.NewScaledQuantity(int64(math.MaxInt64/10+1), 1)}, 36 | {"math.MaxInt64 + 20", uint64(math.MaxInt64 + 20), *resource.NewScaledQuantity(int64(math.MaxInt64/10+2), 1)}, 37 | {"math.MaxInt64 - 10", uint64(math.MaxInt64 - 10), *resource.NewScaledQuantity(int64(math.MaxInt64-10), 0)}, 38 | {"math.MaxInt64 - 100", uint64(math.MaxInt64 - 100), *resource.NewScaledQuantity(int64(math.MaxInt64-100), 0)}, 39 | } 40 | for _, tc := range tcs { 41 | t.Run(tc.name, func(t *testing.T) { 42 | got := uint64Quantity(tc.input, resource.DecimalSI, 0) 43 | if got != tc.expect { 44 | t.Errorf("uint64Quantity(%d, resource.DecimalSI, 0) = %+v, expected: %+v", tc.input, got, tc.expect) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func Test_resourceUsage(t *testing.T) { 51 | start := time.Now() 52 | tcs := []struct { 53 | name string 54 | last MetricsPoint 55 | prev MetricsPoint 56 | wantResourceList v1.ResourceList 57 | wantTimeInfo api.TimeInfo 58 | wantErr bool 59 | }{ 60 | { 61 | name: "get resource usage successfully", 62 | last: newMetricsPoint(start, start.Add(20*time.Millisecond), 500, 600), 63 | prev: newMetricsPoint(start, start.Add(10*time.Millisecond), 300, 400), 64 | wantResourceList: v1.ResourceList{v1.ResourceCPU: uint64Quantity(uint64(20000), resource.DecimalSI, -9), 65 | v1.ResourceMemory: uint64Quantity(600, resource.BinarySI, 0)}, 66 | wantTimeInfo: api.TimeInfo{Timestamp: start.Add(20 * time.Millisecond), Window: 10 * time.Millisecond}, 67 | }, 68 | { 69 | name: "get resource usage failed because of unexpected decrease in startTime", 70 | last: newMetricsPoint(start, start.Add(20*time.Millisecond), 500, 600), 71 | prev: newMetricsPoint(start.Add(20*time.Millisecond), start.Add(10*time.Millisecond), 300, 400), 72 | wantResourceList: v1.ResourceList{}, 73 | wantTimeInfo: api.TimeInfo{}, 74 | wantErr: true, 75 | }, 76 | { 77 | name: "get resource usage failed because of unexpected decrease in cumulative CPU usage value", 78 | last: newMetricsPoint(start, start.Add(20*time.Millisecond), 100, 600), 79 | prev: newMetricsPoint(start, start.Add(10*time.Millisecond), 300, 400), 80 | wantResourceList: v1.ResourceList{}, 81 | wantTimeInfo: api.TimeInfo{}, 82 | wantErr: true, 83 | }, 84 | } 85 | for _, tc := range tcs { 86 | t.Run(tc.name, func(t *testing.T) { 87 | resourceList, timeInfo, err := resourceUsage(tc.last, tc.prev) 88 | if (err != nil) != tc.wantErr { 89 | t.Errorf("resourceUsage() error = %v, wantErr %v", err, tc.wantErr) 90 | return 91 | } 92 | if !reflect.DeepEqual(resourceList, tc.wantResourceList) { 93 | t.Errorf("resourceUsage() resourceList = %v, want %v", resourceList, tc.wantResourceList) 94 | } 95 | if !reflect.DeepEqual(timeInfo, tc.wantTimeInfo) { 96 | t.Errorf("resourceUsage() timeInfo = %v, want %v", timeInfo, tc.wantTimeInfo) 97 | } 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /pkg/utils/address_resolver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package utils 16 | 17 | import ( 18 | "fmt" 19 | 20 | corev1 "k8s.io/api/core/v1" 21 | ) 22 | 23 | var ( 24 | // DefaultAddressTypePriority is the default node address type 25 | // priority list, as taken from the Kubernetes API metrics-server options. 26 | // In general, we prefer overrides to others, internal to external, 27 | // and DNS to IPs. 28 | DefaultAddressTypePriority = []corev1.NodeAddressType{ 29 | // --override-hostname 30 | corev1.NodeHostName, 31 | 32 | // internal, preferring DNS if reported 33 | corev1.NodeInternalDNS, 34 | corev1.NodeInternalIP, 35 | 36 | // external, preferring DNS if reported 37 | corev1.NodeExternalDNS, 38 | corev1.NodeExternalIP, 39 | } 40 | ) 41 | 42 | // NodeAddressResolver knows how to find the preferred connection 43 | // address for a given node. 44 | type NodeAddressResolver interface { 45 | // NodeAddress finds the preferred address to use to connect to 46 | // the given node. 47 | NodeAddress(node *corev1.Node) (address string, err error) 48 | } 49 | 50 | // prioNodeAddrResolver finds node addresses according to a list of 51 | // priorities of types of addresses. 52 | type prioNodeAddrResolver struct { 53 | addrTypePriority []corev1.NodeAddressType 54 | } 55 | 56 | func (r *prioNodeAddrResolver) NodeAddress(node *corev1.Node) (string, error) { 57 | // adapted from k8s.io/kubernetes/pkg/util/node 58 | for _, addrType := range r.addrTypePriority { 59 | for _, addr := range node.Status.Addresses { 60 | if addr.Type == addrType { 61 | return addr.Address, nil 62 | } 63 | } 64 | } 65 | 66 | return "", fmt.Errorf("no address matched types %v", r.addrTypePriority) 67 | } 68 | 69 | // NewPriorityNodeAddressResolver creates a new NodeAddressResolver that resolves 70 | // addresses first based on a list of prioritized address types, then based on 71 | // address order (first to last) within a particular address type. 72 | func NewPriorityNodeAddressResolver(typePriority []corev1.NodeAddressType) NodeAddressResolver { 73 | return &prioNodeAddrResolver{ 74 | addrTypePriority: typePriority, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /pkg/utils/monitoring.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package utils 16 | 17 | import ( 18 | "time" 19 | 20 | "k8s.io/component-base/metrics" 21 | ) 22 | 23 | // BucketsForScrapeDuration calculates a variant of the prometheus default histogram 24 | // buckets that includes relevant buckets around our scrape timeout. 25 | func BucketsForScrapeDuration(scrapeTimeout time.Duration) []float64 { 26 | // set up some buckets that include our scrape timeout, 27 | // so that we can easily pinpoint scrape timeout issues. 28 | // The default buckets provide a sane starting point for 29 | // the smaller numbers. 30 | buckets := append([]float64(nil), metrics.DefBuckets...) 31 | maxBucket := buckets[len(buckets)-1] 32 | timeoutSeconds := float64(scrapeTimeout) / float64(time.Second) 33 | if timeoutSeconds > maxBucket { 34 | // [defaults, (scrapeTimeout + (scrapeTimeout - maxBucket)/ 2), scrapeTimeout, scrapeTimeout*1.5, scrapeTimeout*2] 35 | halfwayToScrapeTimeout := maxBucket + (timeoutSeconds-maxBucket)/2 36 | buckets = append(buckets, halfwayToScrapeTimeout, timeoutSeconds, timeoutSeconds*1.5, timeoutSeconds*2.0) 37 | } else if timeoutSeconds < maxBucket { 38 | var i int 39 | var bucket float64 40 | for i, bucket = range buckets { 41 | if bucket > timeoutSeconds { 42 | break 43 | } 44 | } 45 | 46 | if bucket-timeoutSeconds < buckets[0] || (i > 0 && timeoutSeconds-buckets[i-1] < buckets[0]) { 47 | // if we're sufficiently close to another bucket, just skip this 48 | return buckets 49 | } 50 | 51 | // likely that our scrape timeout is close to another bucket, so don't bother injecting more than just our scrape timeout 52 | oldRest := append([]float64(nil), buckets[i:]...) // make a copy so we don't overwrite it 53 | buckets = append(buckets[:i], timeoutSeconds) 54 | buckets = append(buckets, oldRest...) 55 | } 56 | 57 | return buckets 58 | } 59 | -------------------------------------------------------------------------------- /pkg/utils/monitoring_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package utils 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | . "github.com/onsi/ginkgo/v2" 22 | . "github.com/onsi/gomega" 23 | 24 | "k8s.io/component-base/metrics" 25 | ) 26 | 27 | func TestMetricsUtil(t *testing.T) { 28 | RegisterFailHandler(Fail) 29 | RunSpecs(t, "Prometheus Metrics Util Test") 30 | } 31 | 32 | var _ = Describe("Prometheus Bucket Estimator", func() { 33 | Context("with a scrape timeout longer than the max default bucket", func() { 34 | It("should generate buckets in strictly increasing order", func() { 35 | buckets := BucketsForScrapeDuration(15 * time.Second) 36 | lastBucket := 0.0 37 | for _, bucket := range buckets { 38 | Expect(bucket).To(BeNumerically(">", lastBucket)) 39 | lastBucket = bucket 40 | } 41 | }) 42 | 43 | It("should include some buckets around the scrape timeout", func() { 44 | Expect(BucketsForScrapeDuration(15 * time.Second)).To(ContainElement(15.0)) 45 | Expect(BucketsForScrapeDuration(15 * time.Second)).To(ContainElement(30.0)) 46 | }) 47 | }) 48 | Context("with a scrape timeout shorter than the max default bucket", func() { 49 | It("should generate buckets in strictly increasing order", func() { 50 | buckets := BucketsForScrapeDuration(5 * time.Second) 51 | lastBucket := 0.0 52 | for _, bucket := range buckets { 53 | Expect(bucket).To(BeNumerically(">", lastBucket)) 54 | lastBucket = bucket 55 | } 56 | }) 57 | 58 | It("should include a bucket for the scrape timeout", func() { 59 | Expect(BucketsForScrapeDuration(5 * time.Second)).To(ContainElement(5.0)) 60 | }) 61 | }) 62 | Context("with a scrape timeout equals to the max default bucket", func() { 63 | maxBucket := metrics.DefBuckets[len(metrics.DefBuckets)-1] 64 | maxBucketDuration := time.Duration(maxBucket) * time.Second 65 | 66 | It("should generate buckets in strictly increasing order", func() { 67 | buckets := BucketsForScrapeDuration(maxBucketDuration) 68 | lastBucket := 0.0 69 | for _, bucket := range buckets { 70 | Expect(bucket).To(BeNumerically(">", lastBucket)) 71 | lastBucket = bucket 72 | } 73 | }) 74 | 75 | It("should include a bucket for the scrape timeout", func() { 76 | Expect(BucketsForScrapeDuration(maxBucketDuration)).To(ContainElement(maxBucket)) 77 | }) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /scripts/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | // Copyright The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | -------------------------------------------------------------------------------- /scripts/go.mod: -------------------------------------------------------------------------------- 1 | module sigs.k8s.io/metrics-server/scripts 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/google/addlicense v1.1.1 7 | golang.org/x/perf v0.0.0-20230822165715-3c60af34b3f4 8 | k8s.io/kube-openapi v0.0.0-20230816210353-14e408962443 9 | sigs.k8s.io/logtools v0.9.0 10 | sigs.k8s.io/mdtoc v1.4.0 11 | ) 12 | 13 | require ( 14 | github.com/BurntSushi/toml v0.3.1 // indirect 15 | github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 // indirect 16 | github.com/bmatcuk/doublestar/v4 v4.0.2 // indirect 17 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect 18 | github.com/emicklei/go-restful/v3 v3.8.0 // indirect 19 | github.com/go-logr/logr v0.2.0 // indirect 20 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 21 | github.com/go-openapi/jsonreference v0.20.1 // indirect 22 | github.com/go-openapi/swag v0.22.3 // indirect 23 | github.com/golang/protobuf v1.5.2 // indirect 24 | github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 // indirect 25 | github.com/google/gnostic-models v0.6.8 // indirect 26 | github.com/google/go-cmp v0.6.0 // indirect 27 | github.com/google/gofuzz v1.1.0 // indirect 28 | github.com/inconshreveable/mousetrap v1.1.0 // 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/mmarkdown/mmark v2.0.40+incompatible // 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/rogpeppe/go-internal v1.12.0 // indirect 36 | github.com/spf13/cobra v1.8.1 // indirect 37 | github.com/spf13/pflag v1.0.5 // indirect 38 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect 39 | golang.org/x/mod v0.17.0 // indirect 40 | golang.org/x/sync v0.7.0 // indirect 41 | golang.org/x/tools v0.21.0 // indirect 42 | google.golang.org/protobuf v1.27.1 // indirect 43 | gopkg.in/yaml.v3 v3.0.1 // indirect 44 | k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c // indirect 45 | k8s.io/klog/v2 v2.2.0 // indirect 46 | sigs.k8s.io/release-utils v0.8.3 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /scripts/tools.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Kubernetes Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build tools 16 | 17 | // Package tools tracks dependencies for tools that used in the build process. 18 | // See https://github.com/golang/go/wiki/Modules 19 | package tools 20 | 21 | import ( 22 | _ "github.com/google/addlicense" 23 | _ "golang.org/x/perf/cmd/benchstat" 24 | _ "sigs.k8s.io/logtools/logcheck" 25 | _ "k8s.io/kube-openapi/cmd/openapi-gen" 26 | _ "sigs.k8s.io/mdtoc" 27 | ) 28 | -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2beta29 2 | kind: Config 3 | metadata: 4 | name: metrics-server 5 | build: 6 | local: 7 | useDockerCLI: true 8 | useBuildkit: true 9 | artifacts: 10 | - image: gcr.io/k8s-staging-metrics-server/metrics-server 11 | docker: 12 | dockerfile: Dockerfile 13 | buildArgs: 14 | ARCH: "amd64" 15 | GIT_TAG: "devel" 16 | 17 | profiles: 18 | - name: test 19 | deploy: 20 | kustomize: 21 | paths: 22 | - manifests/overlays/test 23 | - name: test-ha 24 | deploy: 25 | kustomize: 26 | paths: 27 | - manifests/overlays/test-ha 28 | - name: helm 29 | deploy: 30 | helm: 31 | releases: 32 | - name: metrics-server 33 | namespace: kube-system 34 | artifactOverrides: 35 | imageKey: gcr.io/k8s-staging-metrics-server/metrics-server 36 | chartPath: charts/metrics-server 37 | setValueTemplates: 38 | image.repository: "{{.IMAGE_REPO}}" 39 | image.tag: "{{.IMAGE_TAG}}" 40 | setValues: 41 | podLabels: { k8s-app: metrics-server } 42 | containerPort: 10250 43 | valuesFiles: 44 | - test/chart-values.yaml 45 | -------------------------------------------------------------------------------- /test/chart-values.yaml: -------------------------------------------------------------------------------- 1 | args: 2 | - --kubelet-insecure-tls 3 | - --node-selector=metrics-server-skip!=true 4 | -------------------------------------------------------------------------------- /test/kind-config-with-sidecar-containers.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | featureGates: 4 | "SidecarContainers": true 5 | nodes: 6 | - role: control-plane 7 | kubeadmConfigPatches: 8 | - | 9 | kind: ClusterConfiguration 10 | apiServer: 11 | extraArgs: 12 | "enable-aggregator-routing": "true" 13 | - role: worker 14 | - role: worker 15 | labels: 16 | metrics-server-skip: true 17 | 18 | -------------------------------------------------------------------------------- /test/kind-config.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | kubeadmConfigPatches: 6 | - | 7 | kind: ClusterConfiguration 8 | apiServer: 9 | extraArgs: 10 | "enable-aggregator-routing": "true" 11 | - role: worker 12 | - role: worker 13 | labels: 14 | metrics-server-skip: true 15 | -------------------------------------------------------------------------------- /test/test-e2e.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | : ${NODE_IMAGE:?Need to set NODE_IMAGE to test} 6 | : ${SKAFFOLD_PROFILE:="test"} 7 | : ${KIND_CONFIG:="$PWD/test/kind-config.yaml"} 8 | 9 | 10 | KIND_VERSION=0.27.0 11 | SKAFFOLD_VERSION=2.10.1 12 | HELM_VERSION=3.14.3 13 | KUBECTL_VERSION=1.32.3 14 | case $(uname -m) in 15 | x86_64) 16 | ARCH="amd64" 17 | ;; 18 | arm64) 19 | ARCH="arm64" 20 | ;; 21 | *) 22 | echo "Unsupported architecture $(uname -m). Only x86_64 and arm64 are supported." 23 | exit 1 24 | ;; 25 | esac 26 | OS=$(uname -s | tr '[:upper:]' '[:lower:]') 27 | 28 | delete_cluster() { 29 | ${KIND} delete cluster --name=e2e &> /dev/null || true 30 | } 31 | 32 | setup_helm() { 33 | HELM=$(command -v helm || true) 34 | if [[ ${HELM} == "" || $(${HELM} |grep Version |awk -F'Version:' '{print $2}' |awk -F',' '{print $1}') != "\"v${HELM_VERSION}\"" ]] ; then 35 | HELM=_output/helm 36 | fi 37 | if ! [[ $(${HELM} version |grep Version |awk -F'Version:' '{print $2}' |awk -F',' '{print $1}') == "\"v${HELM_VERSION}\"" ]] ; then 38 | echo "helm not found or bad version, downloading binary" 39 | mkdir -p _output 40 | curl -sL "https://get.helm.sh/helm-v${HELM_VERSION}-${OS}-${ARCH}.tar.gz" | tar xz -C _output --strip-components 1 41 | chmod +x _output/helm 42 | HELM=_output/helm 43 | fi 44 | } 45 | 46 | setup_kind() { 47 | KIND=$(command -v kind || true) 48 | if [[ ${KIND} == "" || $(${KIND} --version) != "kind version ${KIND_VERSION}" ]] ; then 49 | KIND=_output/kind 50 | fi 51 | if ! [[ $(${KIND} --version) == "kind version ${KIND_VERSION}" ]] ; then 52 | echo "kind not found or bad version, downloading binary" 53 | mkdir -p _output 54 | curl -sLo _output/kind "https://github.com/kubernetes-sigs/kind/releases/download/v${KIND_VERSION}/kind-${OS}-${ARCH}" 55 | chmod +x _output/kind 56 | KIND=_output/kind 57 | fi 58 | } 59 | 60 | setup_skaffold() { 61 | SKAFFOLD=$(command -v skaffold || true) 62 | if [[ ${SKAFFOLD} == "" || $(${SKAFFOLD} version) != "v${SKAFFOLD_VERSION}" ]] ; then 63 | SKAFFOLD=_output/skaffold 64 | fi 65 | if ! [[ $(${SKAFFOLD} version) == "v${SKAFFOLD_VERSION}" ]] ; then 66 | echo "skaffold not found or bad version, downloading binary" 67 | mkdir -p _output 68 | curl -sLo _output/skaffold "https://storage.googleapis.com/skaffold/releases/v${SKAFFOLD_VERSION}/skaffold-${OS}-${ARCH}" 69 | chmod +x _output/skaffold 70 | SKAFFOLD=_output/skaffold 71 | fi 72 | } 73 | 74 | get_kubectl_version() { 75 | ${KUBECTL} version --client --short 2>&1 | grep 'Client Version: ' | sed 's/^.*: //' 76 | } 77 | 78 | setup_kubectl() { 79 | KUBECTL=$(command -v kubectl || true) 80 | if [[ ${KUBECTL} == "" || $(get_kubectl_version) != "v${KUBECTL_VERSION}" ]] ; then 81 | KUBECTL=_output/kubectl 82 | fi 83 | if ! [[ $(get_kubectl_version) == "v${KUBECTL_VERSION}" ]] ; then 84 | echo "kubectl not found or bad version, downloading binary" 85 | mkdir -p _output 86 | curl -sLo _output/kubectl "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/${OS}/${ARCH}/kubectl" 87 | chmod +x _output/kubectl 88 | KUBECTL=_output/kubectl 89 | fi 90 | } 91 | 92 | create_cluster() { 93 | if ! (${KIND} create cluster --name=e2e --image=${NODE_IMAGE} --config=${KIND_CONFIG}) ; then 94 | echo "Could not create KinD cluster" 95 | exit 1 96 | fi 97 | } 98 | 99 | deploy_metrics_server(){ 100 | PATH="$PWD/_output:${PATH}" ${SKAFFOLD} run -p "${SKAFFOLD_PROFILE}" 101 | sleep 5 102 | } 103 | 104 | run_tests() { 105 | go test test/e2e_test.go -v -count=1 106 | } 107 | 108 | upload_metrics_server_logs(){ 109 | ${KUBECTL} logs -n kube-system -l "k8s-app=metrics-server" > $ARTIFACTS/metrics-server.log 110 | } 111 | 112 | if [ "${SKAFFOLD_PROFILE}" = "helm" ] ; then 113 | setup_helm 114 | fi 115 | setup_kind 116 | setup_skaffold 117 | trap delete_cluster EXIT 118 | delete_cluster 119 | create_cluster 120 | deploy_metrics_server 121 | run_tests 122 | 123 | if [[ ${ARTIFACTS} != "" ]] ; then 124 | setup_kubectl 125 | upload_metrics_server_logs 126 | fi 127 | -------------------------------------------------------------------------------- /test/test-image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | GOARCH=$(go env GOARCH) 6 | 7 | : ${CONTAINER_CLI:?Need to provide environment variable CONTAINER_CLI} 8 | : ${IMAGE:?Need to provide environment variable IMAGE} 9 | : ${EXPECTED_ARCH:?Need to provide variable EXPECTED_ARCH} 10 | : ${EXPECTED_VERSION:?Need to provide environment variable EXPECTED_VERSION} 11 | 12 | echo "CONTAINER CLI ${CONTAINER_CLI}" 13 | 14 | IMAGE_ARCH=$(${CONTAINER_CLI} inspect ${IMAGE} | jq -r ".[].Architecture") 15 | echo "Image architecture ${IMAGE_ARCH}" 16 | 17 | if [[ "${IMAGE_ARCH}" != "${EXPECTED_ARCH}" ]] ; then 18 | echo "Unexpected architecture, got ${IMAGE_ARCH}, expected ${EXPECTED_ARCH}" 19 | exit 1 20 | fi 21 | 22 | if [[ "${IMAGE_ARCH}" == "${GOARCH}" ]] ; then 23 | CONTAINER_VERSION=$(${CONTAINER_CLI} run --rm ${IMAGE} --version) 24 | echo "Image version ${CONTAINER_VERSION}" 25 | 26 | if [[ "${CONTAINER_VERSION}" != "${EXPECTED_VERSION}" ]] ; then 27 | echo "Unexpected binary version, got ${CONTAINER_VERSION}, expected ${EXPECTED_VERSION}" 28 | exit 1 29 | fi 30 | 31 | CLI_HELP="$(${CONTAINER_CLI} run --rm ${IMAGE} --help | sed 's/[ \t]*$//')" 32 | EXPECTED_CLI_HELP="$(cat ./docs/command-line-flags.txt)" 33 | echo "Image help ${CLI_HELP}" 34 | 35 | DIFF=$(diff -u <(echo "${EXPECTED_CLI_HELP}") <(echo "${CLI_HELP}") | tail -n +3 || true) 36 | if [ "$DIFF" ]; then 37 | echo "Unexpected cli help, diff:" 38 | echo "$DIFF" 39 | exit 1 40 | fi 41 | fi 42 | 43 | --------------------------------------------------------------------------------