├── .cr.yaml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ ├── feature_request.md │ ├── issue.md │ └── update_supporting_kubernetes.md ├── dependabot.yml └── workflows │ ├── create-chart-update-pr.yaml │ ├── e2e.yaml │ ├── helm-release.yaml │ ├── helm.yaml │ ├── main.yaml │ ├── pr-labeled.yaml │ ├── project-bot.yaml │ ├── release.yaml │ └── stale.yaml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── RELEASE.md ├── charts └── pvc-autoresizer │ ├── .helmignore │ ├── CHANGELOG.md │ ├── Chart.lock │ ├── Chart.yaml │ ├── README.md │ ├── README.md.gotmpl │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ └── controller │ │ ├── certificate.yaml │ │ ├── clusterrole.yaml │ │ ├── clusterrolebinding.yaml │ │ ├── deployment.yaml │ │ ├── issuer.yaml │ │ ├── mutatingwebhookconfiguration.yaml │ │ ├── podmonitor.yaml │ │ ├── role.yaml │ │ ├── rolebinding.yaml │ │ ├── service.yaml │ │ └── serviceaccount.yaml │ └── values.yaml ├── cmd ├── main.go └── run.go ├── config ├── rbac │ └── role.yaml └── webhook │ └── manifests.yaml ├── constants.go ├── ct.yaml ├── docs ├── design.md ├── maintenance.md └── proposals │ └── resize-when-creating-by-group.md ├── example ├── README.md └── podpvc.yaml ├── go.mod ├── go.sum ├── internal ├── hooks │ └── persistentvolumeclaim.go ├── metrics │ ├── kubernetes_client.go │ ├── kubernetes_client_test.go │ ├── mertics.go │ ├── metrics_client.go │ ├── metrics_client_test.go │ ├── resizer.go │ └── resizer_test.go └── runners │ ├── fake_client_wrapper.go │ ├── k8s_metrics_api_client.go │ ├── metrics_client.go │ ├── metrics_client_test.go │ ├── prometheus_client.go │ ├── pvc_autoresizer.go │ ├── pvc_autoresizer_test.go │ └── suite_test.go ├── test └── e2e │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── autoresizer-cluster.yaml │ ├── manifests │ ├── common │ │ └── storageclass.yaml │ └── values │ │ ├── values-with-metrics-api.yaml │ │ ├── values-without-cert-manager.yaml │ │ └── values.yaml │ ├── suite_test.go │ └── testdata │ └── pod-pvc-template.yaml └── versions.mk /.cr.yaml: -------------------------------------------------------------------------------- 1 | # This file is the config file for helm/chart-releaser 2 | owner: topolvm 3 | git-repo: pvc-autoresizer 4 | release-name-template: "{{ .Name }}-chart-v{{ .Version }}" 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Environments** 14 | - Version: 15 | - OS: 16 | 17 | **To Reproduce** 18 | Steps to reproduce the behavior: 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | **Expected behavior** 25 | A clear and concise description of what you expected to happen. 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Question 3 | url: https://github.com/topolvm/pvc-autoresizer/discussions 4 | about: Support request or question 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: request 6 | assignees: '' 7 | 8 | --- 9 | 10 | 14 | **What should the feature do:** 15 | 16 | **What is use case behind this feature:** 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Task 3 | about: Describe this issue 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## What 11 | 12 | Describe what this issue should address. 13 | 14 | ## How 15 | 16 | Describe how to address the issue. 17 | 18 | ## Checklist 19 | 20 | - [ ] Finish implementation of the issue 21 | - [ ] Test all functions 22 | - [ ] Have enough logs to trace activities 23 | - [ ] Notify developers of necessary actions 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/update_supporting_kubernetes.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Update supporting Kubernetes 3 | about: Dependencies relating to Kubernetes version upgrades 4 | title: 'Update supporting Kubernetes' 5 | labels: 'update kubernetes' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Update Procedure 11 | 12 | - Read [this document](https://github.com/topolvm/pvc-autoresizer/blob/main/docs/maintenance.md). 13 | 14 | ## Before Check List 15 | 16 | There is a check list to confirm depending libraries or tools are released. The release notes for Kubernetes should also be checked. 17 | 18 | ### Must Update Dependencies 19 | 20 | Must update Kubernetes with each new version of Kubernetes. 21 | 22 | - [ ] sigs.k8s.io/controller-runtime 23 | - https://github.com/kubernetes-sigs/controller-runtime/releases 24 | - [ ] sigs.k8s.io/controller-tools 25 | - https://github.com/kubernetes-sigs/controller-tools/releases 26 | - [ ] topolvm 27 | - https://github.com/topolvm/topolvm/blob/main/CHANGELOG.md 28 | 29 | ### Release notes check 30 | 31 | - [ ] Read the necessary release notes for Kubernetes. 32 | 33 | ## Checklist 34 | 35 | - [ ] Finish implementation of the issue 36 | - [ ] Test all functions 37 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | rebase-strategy: "disabled" 6 | schedule: 7 | interval: "monthly" 8 | groups: 9 | github-actions-update: 10 | patterns: 11 | - "*" 12 | -------------------------------------------------------------------------------- /.github/workflows/create-chart-update-pr.yaml: -------------------------------------------------------------------------------- 1 | name: "Create chart update Pull Request" 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | app-version: 7 | description: app version (e.g. 0.1.0) 8 | type: string 9 | chart-version: 10 | description: chart version (e.g. 0.1.0) 11 | type: string 12 | 13 | jobs: 14 | create-chart-update-pr: 15 | runs-on: "ubuntu-latest" 16 | steps: 17 | - name: "Validate input" 18 | run: | 19 | # The exit code will be 1 if the pattern is not found by grep. 20 | echo ${{ inputs.app-version }} | grep -E "^[0-9]+.[0-9]+.[0-9]+$" 21 | echo ${{ inputs.chart-version }} | grep -E "^[0-9]+.[0-9]+.[0-9]+$" 22 | - uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | - name: Configure Git 26 | run: | 27 | # ref. https://github.com/orgs/community/discussions/26560#discussioncomment-3252340 28 | git config user.name "github-actions[bot]" 29 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 30 | - name: "Create a branch" 31 | run: | 32 | git switch main 33 | git pull 34 | git switch -c bump-chart-${{ inputs.chart-version }} 35 | - name: "Update files" 36 | run: | 37 | sed -r -i "s/^version: [[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+/version: ${{ inputs.chart-version }}/g" charts/pvc-autoresizer/Chart.yaml 38 | sed -r -i "s/appVersion: [[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+/appVersion: ${{ inputs.app-version }}/g" charts/pvc-autoresizer/Chart.yaml 39 | sed -r -i "s/ghcr.io\/topolvm\/pvc-autoresizer:[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+/ghcr.io\/topolvm\/pvc-autoresizer:${{ inputs.app-version }}/g" charts/pvc-autoresizer/Chart.yaml 40 | sed -r -i "s/tag: # [[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+/tag: # ${{ inputs.app-version }}/g" charts/pvc-autoresizer/values.yaml 41 | - name: Issue an access token 42 | uses: actions/create-github-app-token@v2 43 | id: app-token 44 | with: 45 | app-id: ${{ secrets.PROJECT_APP_ID }} 46 | private-key: ${{ secrets.PROJECT_APP_PEM }} 47 | - name: "Create pull request" 48 | run: | 49 | git commit -a -s -m "Bump chart version to ${{ inputs.chart-version }}" 50 | git push --set-upstream origin bump-chart-${{ inputs.chart-version }} 51 | gh pr create --draft --fill 52 | env: 53 | GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} 54 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yaml: -------------------------------------------------------------------------------- 1 | name: "e2e" 2 | on: 3 | pull_request: 4 | paths-ignore: 5 | - "**/*.md" 6 | - "CODEOWNERS" 7 | push: 8 | paths-ignore: 9 | - "**/*.md" 10 | - "CODEOWNERS" 11 | branches: 12 | - "main" 13 | jobs: 14 | e2e-k8s: 15 | name: "e2e-k8s" 16 | runs-on: "ubuntu-22.04" 17 | strategy: 18 | matrix: 19 | kubernetes_versions: ["1.32.2", "1.31.6", "1.30.10"] 20 | env: 21 | KUBERNETES_VERSION: ${{ matrix.kubernetes_versions }} 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: "go.mod" 27 | - run: make -C test/e2e setup 28 | - run: make -C test/e2e init-app-with-cert-manager 29 | - run: make -C test/e2e test 30 | 31 | e2e-k8s-without-cert-manager: 32 | name: "e2e-k8s-without-cert-manager" 33 | runs-on: "ubuntu-22.04" 34 | strategy: 35 | matrix: 36 | kubernetes_versions: ["1.32.2", "1.31.6", "1.30.10"] 37 | env: 38 | KUBERNETES_VERSION: ${{ matrix.kubernetes_versions }} 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: actions/setup-go@v5 42 | with: 43 | go-version-file: "go.mod" 44 | - run: make -C test/e2e setup 45 | - run: make -C test/e2e init-app-without-cert-manager 46 | - run: make -C test/e2e test 47 | 48 | e2e-k8s-with-metrics-api: 49 | name: "e2e-k8s-with-metrics-api" 50 | runs-on: "ubuntu-22.04" 51 | strategy: 52 | matrix: 53 | kubernetes_versions: ["1.32.2", "1.31.6", "1.30.10"] 54 | env: 55 | KUBERNETES_VERSION: ${{ matrix.kubernetes_versions }} 56 | steps: 57 | - uses: actions/checkout@v4 58 | - uses: actions/setup-go@v5 59 | with: 60 | go-version-file: "go.mod" 61 | - run: make -C test/e2e setup 62 | - run: make -C test/e2e init-app-with-metrics-api 63 | - run: make -C test/e2e test 64 | -------------------------------------------------------------------------------- /.github/workflows/helm-release.yaml: -------------------------------------------------------------------------------- 1 | name: "Release Charts" 2 | 3 | on: "workflow_dispatch" 4 | 5 | jobs: 6 | release: 7 | runs-on: "ubuntu-latest" 8 | steps: 9 | - name: "Checkout" 10 | uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | 14 | - name: "Configure Git" 15 | run: | 16 | git config user.name "$GITHUB_ACTOR" 17 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 18 | 19 | # Add depending repository for helm to avoid the error below. 20 | # `Error: no repository definition for https://charts.jetstack.io` 21 | # see: https://github.com/helm/chart-releaser-action/issues/74 22 | - name: "Add cert-manager repo for helm" 23 | run: | 24 | helm repo add cert-manager https://charts.jetstack.io 25 | 26 | - name: "Run chart-releaser" 27 | uses: helm/chart-releaser-action@v1.7.0 28 | with: 29 | config: ".cr.yaml" 30 | env: 31 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 32 | -------------------------------------------------------------------------------- /.github/workflows/helm.yaml: -------------------------------------------------------------------------------- 1 | name: "Lint and Test Charts" 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - "charts/**" 7 | 8 | jobs: 9 | lint-test: 10 | runs-on: "ubuntu-22.04" 11 | 12 | steps: 13 | - name: "Checkout" 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: "Setup Go" 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version-file: "go.mod" 22 | 23 | - name: "Setup Tools" 24 | run: | 25 | make setup 26 | 27 | - name: "Run helm-docs" 28 | run: ./bin/helm-docs && git diff --no-patch --exit-code 29 | 30 | - name: "Set up chart-testing" 31 | uses: helm/chart-testing-action@v2.7.0 32 | 33 | - name: "Run chart-testing (lint)" 34 | run: ct lint --config ct.yaml 35 | 36 | - name: "Setup kind" 37 | run: | 38 | make -C test/e2e setup 39 | make -C test/e2e init-cluster 40 | 41 | - name: "Run chart-testing (install)" 42 | run: ct install --config ct.yaml 43 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: "Main" 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - "main" 7 | jobs: 8 | build: 9 | name: "build" 10 | runs-on: "ubuntu-22.04" 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-go@v5 14 | with: 15 | go-version-file: "go.mod" 16 | - run: make setup 17 | - run: make check-uncommitted 18 | - run: make lint 19 | - run: make 20 | - run: make test 21 | - run: env NO_ANNOTATION_CHECK=true make test 22 | - run: make image 23 | -------------------------------------------------------------------------------- /.github/workflows/pr-labeled.yaml: -------------------------------------------------------------------------------- 1 | name: pr 2 | 3 | on: 4 | pull_request: 5 | types: [synchronize, opened, reopened, labeled, unlabeled] 6 | 7 | jobs: 8 | label-do-not-merge: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - run: |- 12 | if [ "${{ contains(join(github.event.pull_request.labels.*.name, ', '), 'do-not-merge') }}" = "true" ]; then 13 | exit 1 14 | fi 15 | exit 0 16 | -------------------------------------------------------------------------------- /.github/workflows/project-bot.yaml: -------------------------------------------------------------------------------- 1 | name: Add a new item to project 2 | on: 3 | issues: 4 | types: 5 | - opened 6 | pull_request_target: 7 | types: 8 | - opened 9 | jobs: 10 | add_item: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Issue an access token 14 | uses: actions/create-github-app-token@v2 15 | id: app-token 16 | with: 17 | app-id: ${{ secrets.PROJECT_APP_ID }} 18 | private-key: ${{ secrets.PROJECT_APP_PEM }} 19 | - name: Get project data 20 | env: 21 | GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} 22 | ORGANIZATION: topolvm 23 | PROJECT_NUMBER: "2" 24 | run: | 25 | proj_id="$(gh api graphql -f query=' 26 | query($org: String!, $number: Int!) { 27 | organization(login: $org){ 28 | projectV2(number: $number) { 29 | id 30 | } 31 | } 32 | }' -F org=$ORGANIZATION -F number=$PROJECT_NUMBER --jq '.data.organization.projectV2.id')" 33 | echo "PROJECT_ID=${proj_id}" >> $GITHUB_ENV 34 | - name: Add an item to project 35 | env: 36 | GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} 37 | run: | 38 | if [ ${{ github.event_name }} = issues ]; then 39 | CONTENT_ID=${{ github.event.issue.node_id }} 40 | else 41 | CONTENT_ID=${{ github.event.pull_request.node_id }} 42 | fi 43 | gh api graphql -f query=' 44 | mutation($project:ID!, $contentId:ID!) { 45 | addProjectV2ItemById(input: {projectId: $project, contentId: $contentId}) { 46 | item { 47 | id 48 | } 49 | } 50 | }' -F project=$PROJECT_ID -F contentId=$CONTENT_ID --jq '.data.addProjectV2ItemById.item.id' 51 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: "Release" 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | jobs: 7 | release: 8 | name: "release" 9 | runs-on: "ubuntu-22.04" 10 | steps: 11 | - name: "Validate Release Version" 12 | id: "check_version" 13 | run: | 14 | VERSION=$(echo $GITHUB_REF | sed -ne 's/[^0-9]*\([0-9]\+\.[0-9]\+\.[0-9]\+\(-.*\)\?\).*/\1/p') 15 | if [ "$VERSION" = "" ]; then 16 | # Invalid version format 17 | exit 1 18 | fi 19 | if [ $(echo $VERSION | grep "-") ]; then PRERELEASE=true; else PRERELEASE=false; fi 20 | echo "version=${VERSION}" >> ${GITHUB_OUTPUT} 21 | echo "prerelease=${PRERELEASE}" >> ${GITHUB_OUTPUT} 22 | - run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version-file: "go.mod" 27 | - run: make setup 28 | - run: make image 29 | - run: make tag IMAGE_TAG=${{ steps.check_version.outputs.version }} 30 | - run: make push IMAGE_TAG=${{ steps.check_version.outputs.version }} 31 | - name: "Push branch tag" 32 | if: ${{ steps.check_version.outputs.prerelease == 'false' }} 33 | run: | 34 | BRANCH=$(echo ${{ steps.check_version.outputs.version }} | cut -d "." -f 1-2) 35 | make tag IMAGE_TAG=$BRANCH 36 | make push IMAGE_TAG=$BRANCH 37 | - name: "Get previous tag" 38 | id: get_previous_tag 39 | run: | 40 | # see https://docs.github.com/en/rest/git/refs?apiVersion=2022-11-28#list-matching-references 41 | RESP=$(gh api \ 42 | -H "Accept: application/vnd.github+json" \ 43 | -H "X-GitHub-Api-Version: 2022-11-28" \ 44 | /repos/${{ github.repository }}/git/matching-refs/tags/v) 45 | PREV_TAG=$(echo ${RESP} | jq -r '.[].ref' | awk -F "/" '{print $3}' | \ 46 | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+" | sort -V -r | tail -n +2 | head -n 1) 47 | if [ -z "${PREV_TAG}" ]; then 48 | echo "PREV_TAG is empty." 49 | exit 1 50 | fi 51 | echo "previous_tag=${PREV_TAG}" >> ${GITHUB_OUTPUT} 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | - name: "Create Release" 55 | id: create_release 56 | run: | 57 | # see https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release 58 | gh api \ 59 | -H "Accept: application/vnd.github+json" \ 60 | --method POST \ 61 | -H "X-GitHub-Api-Version: 2022-11-28" \ 62 | /repos/${{ github.repository }}/releases \ 63 | -f name="Release ${GITHUB_REF_NAME}" \ 64 | -f tag_name="${GITHUB_REF_NAME}" \ 65 | -f previous_tag_name="${{ steps.get_previous_tag.outputs.previous_tag }}" \ 66 | -F draft=true \ 67 | -F prerelease=${{ steps.check_version.outputs.prerelease }} \ 68 | -F generate_release_notes=true 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | -------------------------------------------------------------------------------- /.github/workflows/stale.yaml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: "30 23 * * *" 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v9 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | days-before-stale: 30 25 | stale-issue-message: > 26 | This issue has been automatically marked as stale because it has not had any activity for 30 days. 27 | It will be closed in a week if no further activity occurs. 28 | Thank you for your contributions. 29 | stale-pr-message: > 30 | This pull request has been automatically marked as stale because it has not had any activity for 30 days. 31 | It will be closed in a week if no further activity occurs. 32 | Thank you for your contributions. 33 | close-issue-message: > 34 | This issue has been automatically closed due to inactivity. 35 | Please feel free to reopen this issue (or open a new one) if this still requires investigation. 36 | Thank you for your contribution. 37 | close-pr-message: > 38 | This pull request has been automatically closed due to inactivity. 39 | Please feel free to reopen this issue (or open a new one) if these changes are still required. 40 | Thank you for your contribution. 41 | stale-issue-label: "stale" 42 | stale-pr-label: "stale" 43 | exempt-issue-labels: "keepalive,update kubernetes" 44 | exempt-pr-labels: "keepalive" 45 | exempt-draft-pr: true 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /.vscode 3 | /.idea 4 | /vendor 5 | 6 | # ignore dependency charts 7 | charts/*/charts 8 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 5m 3 | allow-parallel-runners: true 4 | 5 | issues: 6 | # don't skip warning about doc comments 7 | # don't exclude the default set of lint 8 | exclude-use-default: false 9 | # restore some of the defaults 10 | # (fill in the rest as needed) 11 | exclude-rules: 12 | - path: "api/*" 13 | linters: 14 | - lll 15 | - path: "internal/*" 16 | linters: 17 | - dupl 18 | - lll 19 | # add custom rules to allow the same string to be used multiple times to simplify writing the test 20 | - path: "test/*" 21 | linters: 22 | - goconst 23 | linters: 24 | disable-all: true 25 | enable: 26 | - dupl 27 | - errcheck 28 | - copyloopvar 29 | - goconst 30 | - gocyclo 31 | - gofmt 32 | - goimports 33 | - gosimple 34 | - govet 35 | - ineffassign 36 | - lll 37 | - misspell 38 | - nakedret 39 | - prealloc 40 | - staticcheck 41 | - typecheck 42 | - unconvert 43 | - unparam 44 | - unused 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | This file itself is based on [Keep a CHANGELOG](https://keepachangelog.com/en/0.3.0/). 7 | 8 | **Note: See the [release notes](https://github.com/topolvm/pvc-autoresizer/releases) for changes after v0.10.0.** 9 | 10 | ## [Unreleased] 11 | 12 | ## [0.10.0] - 2023-10-13 13 | 14 | ### Changed 15 | 16 | - Support kubernetes 1.27 ([#214](https://github.com/topolvm/pvc-autoresizer/pull/214)) 17 | - let output metrics before the first event when PVC exists ([#212](https://github.com/topolvm/pvc-autoresizer/pull/212)) 18 | 19 | ### Contributors 20 | 21 | - @llamerada-jp 22 | - @toshipp 23 | 24 | ## [0.9.0] - 2023-10-04 25 | 26 | ### Changed 27 | 28 | - Add an item to the check list for Kubernetes upgrade to ensure that t… ([#197](https://github.com/topolvm/pvc-autoresizer/pull/197)) 29 | - prevent e2e test workflow from running when only updating documents ([#198](https://github.com/topolvm/pvc-autoresizer/pull/198)) 30 | - add Ryotaro Banno to owners ([#201](https://github.com/topolvm/pvc-autoresizer/pull/201)) 31 | - Use dependabot grouping feature ([#202](https://github.com/topolvm/pvc-autoresizer/pull/202)) 32 | - drop `resources.limits.storage` support ([#204](https://github.com/topolvm/pvc-autoresizer/pull/204)) 33 | - **BREAKING**: The support of specifying the storage limit by `resources.limits.storage` field of PVCs has been dropped. Please use `resize.topolvm.io/storage_limit` annotation instead. 34 | - Refine exempt-issue-labels to ignore update kubernetes ([#205](https://github.com/topolvm/pvc-autoresizer/pull/205)) 35 | - Replace cybozu/octoken-action with actions/create-github-app-token ([#206](https://github.com/topolvm/pvc-autoresizer/pull/206)) 36 | - Bump the github-actions-update group with 1 update ([#209](https://github.com/topolvm/pvc-autoresizer/pull/209)) 37 | 38 | ### Contributors 39 | 40 | - @peng225 41 | - @llamerada-jp 42 | - @toshipp 43 | 44 | ## [0.8.0] - 2023-05-22 45 | 46 | ### Changed 47 | 48 | - support Kubernetes v1.26 ([#194](https://github.com/topolvm/pvc-autoresizer/pull/194)) 49 | 50 | ### Contributors 51 | 52 | - @peng225 53 | 54 | ## [0.7.0] - 2023-04-05 55 | 56 | ### Added 57 | - proposal: Expand the PVC's initial capacity based on the largest capacity in specified PVCs. ([#174](https://github.com/topolvm/pvc-autoresizer/pull/174)) 58 | - Implement initial-resize-group-by feature ([#176](https://github.com/topolvm/pvc-autoresizer/pull/176)) 59 | - add a workflow job to check the do-not-merge label ([#183](https://github.com/topolvm/pvc-autoresizer/pull/183)) 60 | 61 | ### Changed 62 | - Bump actions/stale from 7 to 8 ([#184](https://github.com/topolvm/pvc-autoresizer/pull/184)) 63 | - Bump actions/setup-go from 3 to 4 ([#185](https://github.com/topolvm/pvc-autoresizer/pull/185)) 64 | 65 | ### Contributors 66 | - @llamerada-jp 67 | - @bells17 68 | - @peng225 69 | 70 | ## [0.6.1] - 2023-02-10 71 | 72 | ### Changed 73 | - update a note descibing how to maintain go version ([#167](https://github.com/topolvm/pvc-autoresizer/pull/167)) 74 | - Update go directive and use the version for setup-go ([#161](https://github.com/topolvm/pvc-autoresizer/pull/161)) 75 | - Use mermaid to draw diagram ([#164](https://github.com/topolvm/pvc-autoresizer/pull/164)) 76 | - Replace quay.io/cybozu/ubuntu with official ubuntu ([#163](https://github.com/topolvm/pvc-autoresizer/pull/163)) 77 | - Add CONTRIBUTING.md and update README.md according to CNCF template. ([#172](https://github.com/topolvm/pvc-autoresizer/pull/172)) 78 | - update go 1.19 to fix ci ([#177](https://github.com/topolvm/pvc-autoresizer/pull/177)) 79 | - Add Signed-off-by on the bump commit ([#178](https://github.com/topolvm/pvc-autoresizer/pull/178)) 80 | 81 | ### Contributors 82 | - @peng225 83 | - @toshipp 84 | - @llamerada-jp 85 | - @cupnes 86 | 87 | ## [0.6.0] - 2023-01-10 88 | 89 | ### Changed 90 | 91 | - Use discussions instead of slack. ([#145](https://github.com/topolvm/pvc-autoresizer/pull/145)) 92 | - Bump actions/stale from 5 to 6 ([#147](https://github.com/topolvm/pvc-autoresizer/pull/147)) 93 | - create generate-.* make target ([#150](https://github.com/topolvm/pvc-autoresizer/pull/150)) 94 | - github/workflows: Use output parameter instead of set-output command ([#151](https://github.com/topolvm/pvc-autoresizer/pull/151)) 95 | - add a command to list the relevant PRs in the release procedure. ([#153](https://github.com/topolvm/pvc-autoresizer/pull/153)) 96 | - add issue template to update supporting kubernetes ([#154](https://github.com/topolvm/pvc-autoresizer/pull/154)) 97 | - update update_supporting_kubernetes.md ([#156](https://github.com/topolvm/pvc-autoresizer/pull/156)) 98 | - support Kubernetes v1.25 ([#159](https://github.com/topolvm/pvc-autoresizer/pull/159)) 99 | - Bump actions/stale from 6 to 7 ([#162](https://github.com/topolvm/pvc-autoresizer/pull/162)) 100 | 101 | ### Contributors 102 | 103 | - @toshipp 104 | - @llamerada-jp 105 | - @pluser 106 | - @peng225 107 | - @cupnes 108 | 109 | ## [0.5.0] - 2022-08-19 110 | 111 | ### Added 112 | 113 | - Add ESASHIKA Kaoru as a reviewer (#139) 114 | 115 | ### Changed 116 | 117 | - e2e: bump TopoLVM version again (#133) 118 | - Add helm repo to README (#137) 119 | - support Kubernetes v1.24 (#136) 120 | 121 | ### Contributors 122 | 123 | - @isaaguilar 124 | 125 | ## [0.4.0] - 2022-07-04 126 | 127 | ### Added 128 | 129 | - add CODEOWNERS (#110) 130 | - Add support for namespace allow list (#120) 131 | - automate adding items to project (#123) 132 | - Update github-actions automatically (#124) 133 | 134 | ### Changed 135 | 136 | - revise CODEOWNERS. (#111) 137 | - generalize curl options (#113) 138 | - Modified to use ghcr.io as a container registry (#114) 139 | - Update e2e topolvm version (#116) 140 | - Bump actions/checkout from 2 to 3 (#125) 141 | - Bump actions/setup-go from 2 to 3 (#127) 142 | - Remove setup-python (#130) 143 | 144 | ### Fixed 145 | 146 | - reconcile: do not resize volume if failed to get inode stats (#121) 147 | 148 | ### Contributors 149 | 150 | - @bells17 151 | - @ryanprobus 152 | 153 | ## [0.3.1] - 2022-04-04 154 | 155 | ### Fixed 156 | - Modify to using a pvc capacity for calculate new storage request (#104) 157 | - inodes threshold doc (#105) 158 | 159 | ### Contributors 160 | - @bells17 161 | 162 | ## [0.3.0] - 2022-03-04 163 | 164 | ### Notice 165 | 166 | The data types of `pvcautoresizer_success_resize_total`, `pvcautoresizer_failed_resize_total` and 167 | `pvcautoresizer_limit_reached_total` are changed to vector. 168 | 169 | ### Changed 170 | - Extend metrics to include pvc name (#93) 171 | 172 | ### Contributors 173 | - @tylerauerbeck 174 | 175 | ## [0.2.3] - 2022-02-07 176 | 177 | ### Changed 178 | - Support Kubernetes v1.23 (#92) 179 | 180 | ### Fixed 181 | - Update example to use preferred storage_limit annotation (#94) 182 | 183 | ### Contributors 184 | - @tylerauerbeck 185 | 186 | ## [0.2.2] - 2022-01-12 187 | 188 | ### Changed 189 | - Support Kubernetes v1.22 (#85) 190 | 191 | ### Contributors 192 | - @bells17 193 | 194 | ## [0.2.1] - 2021-11-01 195 | 196 | ### Added 197 | - Add inode checking feature (#65) 198 | - Storage limit reached (#75) 199 | 200 | ### Fixed 201 | - output error when storage_limit annotation is invalid (#76) 202 | 203 | ### Contributors 204 | - @bells17 205 | - @cmotta2016 206 | 207 | ## [0.2.0] - 2021-09-08 208 | ### Changed 209 | - Change license to Apache License Version 2.0. 210 | 211 | ## [0.1.6] - 2021-08-06 212 | 213 | ### Added 214 | - Expose metrics (#52, #57) 215 | - Add metrics description to README.md (#60) 216 | - Add pvc-autoresizer helm charts (#54) 217 | 218 | ### Changed 219 | - Remove about used_bytes (#49) 220 | 221 | ### Fixed 222 | - Update kubebuilder to v3 (#41) 223 | - Add e2e test (#44) 224 | - Upgrade controller-runtime to v0.9.2 (#47) 225 | - Add parameter tests for resizing (#48) 226 | - Fix e2e image (#53) 227 | 228 | ### Contributors 229 | - @bells17 230 | - @d-kuro 231 | 232 | ## [0.1.5] - 2021-05-06 233 | 234 | ### Notice 235 | 236 | Deprecate specifying an upper limit of volume size with `.spec.resources.limits.storage`. 237 | You can specify the limit by the annotation `resize.topolvm.io/storage_limit`. 238 | 239 | ### Added 240 | - Add support to providing PVC storage limit via annotation (#32) 241 | 242 | ### Changed 243 | - don't crash on a single PVC resizing failure (#33) 244 | 245 | ### Contributors 246 | - @anas-aso 247 | 248 | ## [0.1.4] - 2021-03-22 249 | ### Changed 250 | - Add --no-annotation-check flag (#29) 251 | - Use go 1.16 (#29) 252 | 253 | ## [0.1.3] - 2021-01-25 254 | ### Changed 255 | - Support k8s 1.19 (#21) 256 | - Go 1.15 and Ubuntu 20.04 (#21) 257 | 258 | ## [0.1.2] - 2020-10-14 259 | 260 | ### Changed 261 | 262 | - Increase size calculation is now based on the current storage size (#15). 263 | - Fix Deployment manifest (#14). 264 | 265 | ## [0.1.1] - 2020-10-13 266 | 267 | ### Added 268 | 269 | - Health probes (#11). 270 | 271 | ### Changed 272 | 273 | - Updated manifests (#11). 274 | 275 | ## [0.1.0] - 2020-08-20 276 | 277 | This is the first release. 278 | 279 | ### Contributors 280 | 281 | - @moricho 282 | - @chez-shanpu 283 | 284 | [Unreleased]: https://github.com/topolvm/pvc-autoresizer/compare/v0.10.0...HEAD 285 | [0.10.0]: https://github.com/topolvm/pvc-autoresizer/compare/v0.9.0...v0.10.0 286 | [0.9.0]: https://github.com/topolvm/pvc-autoresizer/compare/v0.8.0...v0.9.0 287 | [0.8.0]: https://github.com/topolvm/pvc-autoresizer/compare/v0.7.0...v0.8.0 288 | [0.7.0]: https://github.com/topolvm/pvc-autoresizer/compare/v0.6.1...v0.7.0 289 | [0.6.1]: https://github.com/topolvm/pvc-autoresizer/compare/v0.6.0...v0.6.1 290 | [0.6.0]: https://github.com/topolvm/pvc-autoresizer/compare/v0.5.0...v0.6.0 291 | [0.5.0]: https://github.com/topolvm/pvc-autoresizer/compare/v0.4.0...v0.5.0 292 | [0.4.0]: https://github.com/topolvm/pvc-autoresizer/compare/v0.3.1...v0.4.0 293 | [0.3.1]: https://github.com/topolvm/pvc-autoresizer/compare/v0.3.0...v0.3.1 294 | [0.3.0]: https://github.com/topolvm/pvc-autoresizer/compare/v0.2.3...v0.3.0 295 | [0.2.3]: https://github.com/topolvm/pvc-autoresizer/compare/v0.2.2...v0.2.3 296 | [0.2.2]: https://github.com/topolvm/pvc-autoresizer/compare/v0.2.1...v0.2.2 297 | [0.2.1]: https://github.com/topolvm/pvc-autoresizer/compare/v0.2.0...v0.2.1 298 | [0.2.0]: https://github.com/topolvm/pvc-autoresizer/compare/v0.1.6...v0.2.0 299 | [0.1.6]: https://github.com/topolvm/pvc-autoresizer/compare/v0.1.5...v0.1.6 300 | [0.1.5]: https://github.com/topolvm/pvc-autoresizer/compare/v0.1.4...v0.1.5 301 | [0.1.4]: https://github.com/topolvm/pvc-autoresizer/compare/v0.1.3...v0.1.4 302 | [0.1.3]: https://github.com/topolvm/pvc-autoresizer/compare/v0.1.2...v0.1.3 303 | [0.1.2]: https://github.com/topolvm/pvc-autoresizer/compare/v0.1.1...v0.1.2 304 | [0.1.1]: https://github.com/topolvm/pvc-autoresizer/compare/v0.1.0...v0.1.1 305 | [0.1.0]: https://github.com/topolvm/pvc-autoresizer/compare/ee8a31ac32b1ad40f0bace32317aa1eee4a8225c...v0.1.0 306 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Maintainers can approve and merge PRs. 2 | # @toshipp: Toshikuni Fukaya 3 | # @llamerada-jp: Yuji Ito 4 | # @satoru-takeuchi: Satoru Takeuchi 5 | # @cupnes: Yuma Ohgami 6 | # @daichimukai: Daichi Mukai 7 | # @peng225: Shinya Hayashi 8 | # @pluser: ESASHIKA Kaoru 9 | # @ushitora-anqou: Ryotaro Banno 10 | * @topolvm/reviewers 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | - [Ways to Contribute](#ways-to-contribute) 4 | - [Find an Issue](#find-an-issue) 5 | - [Ask for Help](#ask-for-help) 6 | - [Pull Request Lifecycle](#pull-request-lifecycle) 7 | - [Development Environment Setup](#development-environment-setup) 8 | - [Sign Your Commits](#sign-your-commits) 9 | - [DCO](#dco) 10 | - [Pull Request Checklist](#pull-request-checklist) 11 | 12 | Welcome! We are glad that you want to contribute to our project! 13 | 14 | As you get started, you are in the best position to give us feedback on areas of 15 | our project that we need help with including: 16 | 17 | * Problems found during setting up a new developer environment 18 | * Gaps in our Quickstart Guide or documentation 19 | * Bugs in our automation scripts 20 | 21 | If anything doesn't make sense, or doesn't work when you run it, please open a 22 | bug report and let us know! 23 | 24 | ## Ways to Contribute 25 | 26 | We welcome many different types of contributions including: 27 | 28 | * New features 29 | * Builds, CI/CD 30 | * Bug fixes 31 | * Documentation 32 | * Issue Triage 33 | * Answering questions on GitHub Discussions 34 | * Communications / Social Media / Blog Posts 35 | * Release management 36 | 37 | Not everything happens through a GitHub pull request. Please [contact us](https://github.com/topolvm/pvc-autoresizer/discussions) 38 | and let's discuss how we can work together. 39 | 40 | ## Find an Issue 41 | 42 | We have good first issues for new contributors and help wanted issues suitable 43 | for any contributor. [good first issue](https://github.com/topolvm/pvc-autoresizer/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) has extra information to 44 | help you make your first contribution. [help wanted](https://github.com/topolvm/pvc-autoresizer/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) are issues 45 | suitable for someone who isn't a core maintainer and is good to move onto after 46 | your first pull request. 47 | 48 | Sometimes there won’t be any issues with these labels. That’s ok! There is 49 | likely still something for you to work on. If you want to contribute but you 50 | don’t know where to start or can't find a suitable issue, you can ask for an 51 | issue to work on the [Discussions](https://github.com/topolvm/pvc-autoresizer/discussions). 52 | 53 | Once you see an issue that you'd like to work on, please post a comment saying 54 | that you want to work on it. Something like "I want to work on this" is fine. 55 | 56 | ## Ask for Help 57 | 58 | The best way to reach us with a question when contributing is to ask on: 59 | 60 | * The original github issue 61 | 62 | ## Pull Request Lifecycle 63 | 64 | If you want to make a non-trivial change, such as a new feature or a bug fix to break exiting behavior, 65 | please discuss it first with maintainers on a issue, discussion or a proposal PR you send. 66 | 67 | When your PR becomes ready for review, it isn't a draft and all tests pass, we will start 68 | a review process. Reviewers are selected automatically, so you don't care about them. 69 | 70 | If your change is approved by us, we will merge it immediately and it will be 71 | shipped on the next monthly release. 72 | 73 | When there has been no activity for 30 days, the stale bot will label it stale. 74 | And if there is no activity for another 7 days, it will be closed. 75 | 76 | ## Development Environment Setup 77 | 78 | Our recommended environment is Ubuntu 22.04. Because following steps modify your system globally, 79 | we suggest preparing a dedicated physical or virtual machine. 80 | 81 | 1. Download the repository. 82 | 83 | ```console 84 | git clone https://github.com/topolvm/pvc-autoresizer.git 85 | ``` 86 | 87 | 2. Install the required tools. 88 | 89 | ```console 90 | cd pvc-autoresizer 91 | make setup 92 | ``` 93 | 94 | 3. Make changes you wish. 95 | 96 | 4. Test your changes. 97 | 98 | ```console 99 | # for unit test and lint 100 | make test 101 | 102 | # for end-to-end test 103 | cd test/e2e 104 | make setup 105 | make init-cluster 106 | make test 107 | ``` 108 | 109 | ## Sign Your Commits 110 | 111 | ### DCO 112 | Licensing is important to open source projects. It provides some assurances that 113 | the software will continue to be available based under the terms that the 114 | author(s) desired. We require that contributors sign off on commits submitted to 115 | our project's repositories. The [Developer Certificate of Origin 116 | (DCO)](https://probot.github.io/apps/dco/) is a way to certify that you wrote and 117 | have the right to contribute the code you are submitting to the project. 118 | 119 | You sign-off by adding the following to your commit messages. Your sign-off must 120 | match the git user and email associated with the commit. 121 | 122 | This is my commit message 123 | 124 | Signed-off-by: Your Name 125 | 126 | Git has a `-s` command line option to do this automatically: 127 | 128 | git commit -s -m 'This is my commit message' 129 | 130 | If you forgot to do this and have not yet pushed your changes to the remote 131 | repository, you can amend your commit with the sign-off by running 132 | 133 | git commit --amend -s 134 | 135 | ## Pull Request Checklist 136 | 137 | When you submit your pull request, or you push new commits to it, our automated 138 | systems will run some checks on your new code. We require that your pull request 139 | passes these checks, but we also have more criteria than just that before we can 140 | accept and merge it. We recommend that you check the following things locally 141 | before you submit your code: 142 | 143 | - If your code has breaking changes, please update related documents. 144 | - If you add a new feature, please add unit or end-to-end tests for it. 145 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage1: Build the pvc-autoresizer binary 2 | FROM golang:1.23 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # Copy the go source 9 | COPY constants.go constants.go 10 | COPY cmd/ cmd/ 11 | COPY internal/ internal/ 12 | 13 | # Build 14 | RUN CGO_ENABLED=0 go build -ldflags="-w -s" -a -o pvc-autoresizer cmd/*.go 15 | 16 | # Stage2: setup runtime container 17 | FROM scratch 18 | WORKDIR / 19 | COPY --from=builder /workspace/pvc-autoresizer . 20 | EXPOSE 8080 21 | USER 10000:10000 22 | 23 | ENTRYPOINT ["/pvc-autoresizer"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for pvc-autoresizer 2 | include versions.mk 3 | 4 | ## DON'T EDIT BELOW THIS LINE 5 | GOOS := $(shell go env GOOS) 6 | GOARCH := $(shell go env GOARCH) 7 | 8 | CRD_OPTIONS = "crd:crdVersions=v1" 9 | 10 | BINDIR := $(shell pwd)/bin 11 | CONTROLLER_GEN := $(BINDIR)/controller-gen 12 | GOLANGCI_LINT = $(BINDIR)/golangci-lint 13 | KUBECTL := $(BINDIR)/kubectl 14 | KUSTOMIZE := $(BINDIR)/kustomize 15 | 16 | KUBEBUILDER_ASSETS := $(BINDIR) 17 | export KUBEBUILDER_ASSETS 18 | 19 | IMAGE_TAG ?= latest 20 | IMAGE_PREFIX ?= ghcr.io/topolvm/ 21 | 22 | # Setting SHELL to bash allows bash commands to be executed by recipes. 23 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 24 | SHELL = /usr/bin/env bash -o pipefail 25 | .SHELLFLAGS = -ec 26 | 27 | .PHONY: all 28 | all: build 29 | 30 | ##@ General 31 | 32 | # The help target prints out all targets with their descriptions organized 33 | # beneath their categories. The categories are represented by '##@' and the 34 | # target descriptions by '##'. The awk commands is responsible for reading the 35 | # entire set of makefiles included in this invocation, looking for lines of the 36 | # file as xyz: ## something, and then pretty-format the target and help. Then, 37 | # if there's a line with ##@ something, that gets pretty-printed as a category. 38 | # More info on the usage of ANSI control characters for terminal formatting: 39 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 40 | # More info on the awk command: 41 | # http://linuxcommand.org/lc3_adv_awk.php 42 | 43 | .PHONY: help 44 | help: ## Display this help. 45 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 46 | 47 | ##@ Development 48 | 49 | .PHONY: manifests 50 | manifests: ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 51 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=controller webhook paths="./..." output:crd:artifacts:config=config/crd/bases 52 | 53 | .PHONY: generate-helm-docs 54 | generate-helm-docs: 55 | ./bin/helm-docs -c charts/pvc-autoresizer/ 56 | 57 | .PHONY: generate 58 | generate: manifests generate-helm-docs 59 | 60 | .PHONY: check-uncommitted 61 | check-uncommitted: generate ## Check if latest generated artifacts are committed. 62 | git diff --exit-code --name-only 63 | 64 | .PHONY: fmt 65 | fmt: ## Run go fmt against code. 66 | test -z "$$(gofmt -s -l . | tee /dev/stderr)" 67 | 68 | .PHONY: vet 69 | vet: ## Run go vet against code. 70 | go vet ./... 71 | 72 | .PHONY: test 73 | test: manifests generate tools fmt vet ## Run tests. 74 | $(shell go env GOPATH)/bin/staticcheck ./... 75 | go install ./... 76 | source <($(SETUP_ENVTEST) use -p env $(ENVTEST_K8S_VERSION)); \ 77 | go test -race -v -count 1 ./... --timeout=60s 78 | 79 | .PHONY: lint 80 | lint: ## Run golangci-lint linter & yamllint 81 | $(GOLANGCI_LINT) run --timeout 3m 82 | 83 | .PHONY: lint-fix 84 | lint-fix: ## Run golangci-lint linter and perform fixes 85 | $(GOLANGCI_LINT) run --fix 86 | 87 | ##@ Build 88 | 89 | .PHONY: build 90 | build: ## Build manager binary. 91 | go build -o $(BINDIR)/manager ./cmd/* 92 | 93 | .PHONY: run 94 | run: manifests generate ## Run a controller from your host. 95 | go run ./cmd/main.go 96 | 97 | .PHONY: image 98 | image: ## Build docker image. 99 | docker build . -t $(IMAGE_PREFIX)pvc-autoresizer:devel 100 | 101 | .PHONY: tag 102 | tag: ## Set a docker tag to the image. 103 | docker tag $(IMAGE_PREFIX)pvc-autoresizer:devel $(IMAGE_PREFIX)pvc-autoresizer:$(IMAGE_TAG) 104 | 105 | .PHONY: push 106 | push: ## Push docker image. 107 | docker push $(IMAGE_PREFIX)pvc-autoresizer:$(IMAGE_TAG) 108 | 109 | ##@ Chart Testing 110 | 111 | .PHONY: ct-lint 112 | ct-lint: ## Lint and validate a chart. 113 | docker run \ 114 | --rm \ 115 | --user $(shell id -u $(USER)) \ 116 | --workdir=/data \ 117 | --volume $(shell pwd):/data \ 118 | quay.io/helmpack/chart-testing:v$(CHART_TESTING_VERSION) \ 119 | ct lint --config ct.yaml 120 | 121 | .PHONY: ct-install 122 | ct-install: ## Install and test a chart. 123 | docker run \ 124 | --rm \ 125 | --user $(shell id -u $(USER)) \ 126 | --network host \ 127 | --workdir=/data \ 128 | --env KUBECONFIG=/kubeconfig \ 129 | --volume ~/.kube/config:/kubeconfig:ro \ 130 | --volume $(shell pwd):/data \ 131 | quay.io/helmpack/chart-testing:v$(CHART_TESTING_VERSION) \ 132 | ct install --config ct.yaml 133 | 134 | ##@ Tools 135 | 136 | .PHONY: tools 137 | tools: staticcheck setup-envtest 138 | 139 | .PHONY: staticcheck 140 | staticcheck: ## Install staticcheck 141 | if ! which staticcheck >/dev/null; then \ 142 | env GOFLAGS= go install honnef.co/go/tools/cmd/staticcheck@latest; \ 143 | fi 144 | 145 | SETUP_ENVTEST := $(BINDIR)/setup-envtest 146 | .PHONY: setup-envtest 147 | setup-envtest: $(SETUP_ENVTEST) ## Download setup-envtest locally if necessary 148 | $(SETUP_ENVTEST): 149 | # see https://github.com/kubernetes-sigs/controller-runtime/tree/master/tools/setup-envtest 150 | GOBIN=$(BINDIR) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@$(ENVTEST_BRANCH) 151 | 152 | .PHONY: setup 153 | setup: # Setup tools 154 | mkdir -p bin 155 | GOBIN=$(BINDIR) go install sigs.k8s.io/controller-tools/cmd/controller-gen@v$(CONTROLLER_TOOLS_VERSION) 156 | curl -o $(KUBECTL) -sSfL https://dl.k8s.io/release/v$(KUBERNETES_VERSION)/bin/linux/amd64/kubectl 157 | chmod a+x $(KUBECTL) 158 | GOBIN=$(BINDIR) go install github.com/norwoodj/helm-docs/cmd/helm-docs@v$(HELM_DOCS_VERSION) 159 | curl -sSfL https://get.helm.sh/helm-v$(HELM_VERSION)-linux-amd64.tar.gz \ 160 | | tar xvz -C $(BINDIR) --strip-components 1 linux-amd64/helm 161 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell dirname $(GOLANGCI_LINT)) $(GOLANGCI_LINT_VERSION) 162 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: topolvm.io 6 | layout: 7 | - go.kubebuilder.io/v4 8 | projectName: pvc-autoresizer 9 | repo: github.com/topolvm/pvc-autoresizer 10 | version: "3" 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub release](https://img.shields.io/github/v/release/topolvm/pvc-autoresizer.svg?maxAge=60)][releases] 2 | [![Main](https://github.com/topolvm/pvc-autoresizer/workflows/Main/badge.svg)](https://github.com/topolvm/pvc-autoresizer/actions) 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/topolvm/pvc-autoresizer?tab=overview)](https://pkg.go.dev/github.com/topolvm/pvc-autoresizer?tab=overview) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/topolvm/pvc-autoresizer)](https://goreportcard.com/badge/github.com/topolvm/pvc-autoresizer) 5 | 6 | # Welcome to the pvc-autoresizer Project! 7 | 8 | `pvc-autoresizer` resizes PersistentVolumeClaims (PVCs) when the free amount of storage is below the threshold. 9 | 10 | It queries the volume usage metrics from Prometheus that collects metrics from `kubelet`. 11 | 12 | Our supported platforms are: 13 | 14 | - Kubernetes: 1.32, 1.31, 1.30 15 | - CSI drivers that implements the following features 16 | - [Volume Expansion](https://kubernetes-csi.github.io/docs/volume-expansion.html) 17 | - [NodeGetVolumeStats](https://github.com/container-storage-interface/spec/blob/master/spec.md#nodegetvolumestats) 18 | 19 | Container images are available on [ghcr.io](https://github.com/topolvm/pvc-autoresizer/pkgs/container/pvc-autoresizer). 20 | 21 | ## Getting Started 22 | 23 | ### Prepare 24 | 25 | `pvc-autoresizer` behaves based on the metrics that prometheus collects from kubelet. 26 | 27 | Please refer to the following pages to set up Prometheus: 28 | 29 | - [Installation | Prometheus](https://prometheus.io/docs/prometheus/latest/installation/) 30 | 31 | In addition, configure scraping as follows: 32 | 33 | - [Monitoring with Prometheus | TopoLVM](https://github.com/topolvm/topolvm/blob/master/docs/prometheus.md) 34 | 35 | ### Installation 36 | 37 | Specify the Prometheus URL to `pvc-autoresizer` argument as `--prometheus-url`. 38 | 39 | `pvc-autoresizer` can be deployed to a Kubernetes cluster via `helm`: 40 | 41 | ```sh 42 | helm repo add pvc-autoresizer https://topolvm.github.io/pvc-autoresizer/ 43 | helm install --create-namespace --namespace pvc-autoresizer pvc-autoresizer pvc-autoresizer/pvc-autoresizer --set "controller.args.prometheusURL=" 44 | ``` 45 | 46 | See the Chart [README.md](./charts/pvc-autoresizer/README.md) for detailed documentation on the Helm Chart. 47 | 48 | ### How to use 49 | 50 | To allow auto volume expansion, the StorageClass of PVC need to allow volume expansion and 51 | have `resize.topolvm.io/enabled: "true"` annotation. The annotation may be omitted if 52 | you give `--no-annotation-check` command-line flag to `pvc-autoresizer` executable. 53 | 54 | ```yaml 55 | kind: StorageClass 56 | apiVersion: storage.k8s.io/v1 57 | metadata: 58 | name: topolvm-provisioner 59 | annotations: 60 | resize.topolvm.io/enabled: "true" 61 | provisioner: topolvm.io 62 | allowVolumeExpansion: true 63 | ``` 64 | 65 | To allow auto volume expansion, the PVC to be resized needs to specify the upper limit of 66 | volume size with the annotation `resize.topolvm.io/storage_limit`. 67 | The value of `resize.topolvm.io/storage_limit` should not be zero, 68 | or the annotation will be ignored. 69 | 70 | The PVC must have `volumeMode: Filesystem`, too. 71 | 72 | ```yaml 73 | kind: PersistentVolumeClaim 74 | apiVersion: v1 75 | metadata: 76 | name: topolvm-pvc 77 | namespace: default 78 | annotations: 79 | resize.topolvm.io/storage_limit: 100Gi 80 | spec: 81 | accessModes: 82 | - ReadWriteOnce 83 | volumeMode: Filesystem 84 | resources: 85 | requests: 86 | storage: 30Gi 87 | storageClassName: topolvm-provisioner 88 | ``` 89 | 90 | The PVC can optionally have `resize.topolvm.io/threshold`, `resize.topolvm.io/inodes-threshold` and `resize.topolvm.io/increase` annotations. 91 | (If they are not given, the default value is `10%`.) 92 | 93 | When the amount of free space of the volume is below `resize.topolvm.io/threshold` 94 | or the number of free inodes is below `resize.topolvm.io/inodes-threshold`, 95 | `.spec.resources.requests.storage` is increased by `resize.topolvm.io/increase`. 96 | 97 | If `resize.topolvm.io/increase` is given as a percentage, the value is calculated as 98 | the current `spec.resources.requests.storage` value multiplied by the annotation value. 99 | 100 | ```yaml 101 | kind: PersistentVolumeClaim 102 | apiVersion: v1 103 | metadata: 104 | name: topolvm-pvc 105 | namespace: default 106 | annotations: 107 | resize.topolvm.io/storage_limit: 100Gi 108 | resize.topolvm.io/threshold: 20% 109 | resize.topolvm.io/inodes-threshold: 20% 110 | resize.topolvm.io/increase: 20Gi 111 | spec: 112 | 113 | ``` 114 | 115 | #### Initial resize 116 | 117 | PVC request size can also be changed at the creation time based on the largest PVC size in the same group. PVCs are grouped by labels, and the label key for grouping is specified by `resize.topolvm.io/initial-resize-group-by` annotation. 118 | 119 | For example, suppose there are following three PVCs. 120 | 121 | ```yaml 122 | ### existing PVCs (excerpted) 123 | kind: PersistentVolumeClaim 124 | metadata: 125 | name: pvc-x-1 126 | labels: 127 | label-foobar: group-x 128 | annotations: 129 | resize.topolvm.io/initial-resize-group-by: label-foobar 130 | spec: 131 | resources: 132 | requests: 133 | storage: 20Gi 134 | 135 | kind: PersistentVolumeClaim 136 | metadata: 137 | name: pvc-x-2 138 | labels: 139 | label-foobar: group-x 140 | annotations: 141 | resize.topolvm.io/initial-resize-group-by: label-foobar 142 | spec: 143 | resources: 144 | requests: 145 | storage: 16Gi 146 | 147 | kind: PersistentVolumeClaim 148 | metadata: 149 | name: pvc-y-1 150 | labels: 151 | label-foobar: group-y 152 | annotations: 153 | resize.topolvm.io/initial-resize-group-by: label-foobar 154 | spec: 155 | resources: 156 | requests: 157 | storage: 30Gi 158 | ``` 159 | 160 | When creating the following new PVC, `pvc-x-1` and `pvc-x-2` with `label-foobar: group-x` are considered to be in the same group, and `pvc-y-1` is not. Therefore, the PVC is created with **20Gi** based on `pvc-x-1`, which has the largest capacity in the group. 161 | 162 | ```yaml 163 | kind: PersistentVolumeClaim 164 | metadata: 165 | name: pvc-x-3 166 | labels: 167 | label-foobar: group-x 168 | annotations: 169 | resize.topolvm.io/initial-resize-group-by: label-foobar 170 | spec: 171 | resources: 172 | requests: 173 | storage: 10Gi 174 | ``` 175 | 176 | When creating the following new PVC, `pvc-y-1` with `label-foobar: group-y` is in the same group. However, since the new PVC's size(50Gi) is larger than the existing one(30Gi), the PVC is created with **50Gi**. 177 | 178 | ```yaml 179 | kind: PersistentVolumeClaim 180 | metadata: 181 | name: pvc-y-2 182 | labels: 183 | label-foobar: group-y 184 | annotations: 185 | resize.topolvm.io/initial-resize-group-by: label-foobar 186 | spec: 187 | resources: 188 | requests: 189 | storage: 50Gi 190 | ``` 191 | 192 | When the size of the largest PVC in the same group is larger than the value set to `resize.topolvm.io/storage_limit` annotation, 193 | the PVC is resized up to this limit. 194 | 195 | ### Prometheus metrics 196 | 197 | #### `pvcautoresizer_kubernetes_client_fail_total` 198 | 199 | `pvcautoresizer_kubernetes_client_fail_total` is a counter that indicates how many API requests to kube-api server are failed. 200 | 201 | #### `pvcautoresizer_metrics_client_fail_total` 202 | 203 | `pvcautoresizer_metrics_client_fail_total` is a counter that indicates how many API requests to metrics server(e.g. prometheus) are failed. 204 | 205 | #### `pvcautoresizer_loop_seconds_total` 206 | 207 | `pvcautoresizer_loop_seconds_total` is a counter that indicates the sum of seconds spent on volume expansion processing loops. 208 | 209 | #### `pvcautoresizer_success_resize_total` 210 | 211 | `pvcautoresizer_success_resize_total` is a counter that indicates how many volume expansion processing resizes succeed. 212 | 213 | #### `pvcautoresizer_failed_resize_total` 214 | 215 | `pvcautoresizer_failed_resize_total` is a counter that indicates how many volume expansion processing resizes fail. 216 | 217 | #### `pvcautoresizer_limit_reached_total` 218 | 219 | `pvcautoresizer_limit_reached_total` is a counter that indicates how many storage limit was reached. 220 | 221 | ## Contributing 222 | 223 | pvc-autoresizer project welcomes contributions from any member of our community. To get 224 | started contributing, please see our [Contributor Guide](CONTRIBUTING.md). 225 | 226 | ## Communications 227 | 228 | If you have any questions or ideas, please use [discussions](https://github.com/topolvm/topolvm/discussions). 229 | 230 | ## Resources 231 | 232 | [docs](docs/) directory contains designs, and so on. 233 | 234 | ## License 235 | 236 | This project is licensed under [Apache License 2.0](LICENSE). 237 | 238 | [releases]: https://github.com/topolvm/pvc-autoresizer/releases 239 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | Release procedure 2 | ================= 3 | 4 | This document describes how to release a new version of pvc-autoresizer. 5 | 6 | Versioning 7 | ---------- 8 | 9 | Follow [semantic versioning 2.0.0][semver] to choose the new version number. 10 | 11 | The format of release notes 12 | --------------------------- 13 | 14 | In the release procedure for both the app and Helm Chart, the release note is generated automatically, 15 | and then it is edited manually. In this step, PRs should be classified based on [Keep a CHANGELOG](https://keepachangelog.com/en/1.1.0/). 16 | 17 | The result should look something like: 18 | 19 | ```markdown 20 | ## What's Changed 21 | 22 | ### Added 23 | 24 | * Add a notable feature for users (#35) 25 | 26 | ### Changed 27 | 28 | * Change a behavior affecting users (#33) 29 | 30 | ### Removed 31 | 32 | * Remove a feature, users action required (#39) 33 | 34 | ### Fixed 35 | 36 | * Fix something not affecting users or a minor change (#40) 37 | ``` 38 | 39 | Bump version 40 | ------------ 41 | 42 | 1. Check if a new release is required by [checking the differences](https://github.com/topolvm/pvc-autoresizer/compare/vX.Y.Z...main) since the last release. If so, determine a new version number and define the `VERSION` variable. 43 | 44 | ```console 45 | VERSION=1.2.3 46 | ``` 47 | 48 | 2. Go to [the rule setting page](https://github.com/topolvm/pvc-autoresizer/settings/rules/2151118) and change the value of "Enforcement status" to `Active`. 49 | 50 | 3. Add a new tag and push it as follows: 51 | 52 | ```console 53 | git switch main 54 | git pull 55 | git tag v$VERSION 56 | git push origin v$VERSION 57 | ``` 58 | 59 | 4. Once a new tag is pushed, [GitHub Actions][] automatically 60 | creates a draft release note for the tagged version, 61 | builds a tar archive for the new release, 62 | and attaches it to the release note. 63 | 64 | Visit https://github.com/topolvm/pvc-autoresizer/releases to check 65 | the result. 66 | 67 | 5. Edit the auto-generated release note 68 | and remove PRs which contain changes only to the helm chart. 69 | Then, publish it. 70 | 71 | Release Helm Chart 72 | ----------------- 73 | 74 | pvc-autoresizer Helm Chart will be released independently from pvc-autoresizer's release. 75 | This will prevent the pvc-autoresizer version from going up just by modifying the Helm Chart. 76 | 77 | You must change the version of Chart.yaml when making changes to the Helm Chart. CI fails with lint error when creating a Pull Request without changing the version of Chart.yaml. 78 | 79 | 1. If necessary, go to [the rule setting page](https://github.com/topolvm/pvc-autoresizer/settings/rules/2151118) and change the value of "Enforcement status" to `Active`. 80 | - If you are here after releasing the app, you should already have this setting changed. 81 | 82 | 2. Determine a new version number by [checking the differences](https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-vX.Y.Z...main) since the last release. Then, manually run the workflow to create a PR to update the Helm Chart. 83 | 84 | https://github.com/topolvm/pvc-autoresizer/actions/workflows/create-chart-update-pr.yaml 85 | 86 | 3. Review and merge the auto-created PR. 87 | - Before merging, go to [the rule setting page](https://github.com/topolvm/pvc-autoresizer/settings/rules/2151118) and change the value of "Enforcement status" to `Disabled`. 88 | 89 | 4. Manually run the GitHub Actions workflow for the release. 90 | 91 | https://github.com/topolvm/pvc-autoresizer/actions/workflows/helm-release.yaml 92 | 93 | When you run workflow, [helm/chart-releaser-action](https://github.com/helm/chart-releaser-action) will automatically create a GitHub Release. 94 | 95 | 5. Edit the auto-generated release note as follows: 96 | 1. Select the "Previous tag", which is in the form of "pvc-autoresizer-chart-vX.Y.Z". 97 | 2. Clear the textbox, and click "Generate release notes" button. 98 | 3. Remove PRs which do not contain changes to the helm chart. 99 | 100 | [semver]: https://semver.org/spec/v2.0.0.html 101 | [example]: https://github.com/cybozu-go/etcdpasswd/commit/77d95384ac6c97e7f48281eaf23cb94f68867f79 102 | [GitHub Actions]: https://github.com/topolvm/pvc-autoresizer/actions 103 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/.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/pvc-autoresizer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | This file itself is based on [Keep a CHANGELOG](https://keepachangelog.com/en/0.3.0/). 7 | 8 | **Note: See the [release notes](https://github.com/topolvm/pvc-autoresizer/releases) for changes after v0.10.0.** 9 | 10 | ## [Unreleased] 11 | 12 | ## [0.10.0] - 2023-10-16 13 | 14 | ### Changed 15 | 16 | - appVersion was changed to 0.10.0. 17 | 18 | ## [0.9.0] - 2023-10-05 19 | 20 | ### Added 21 | 22 | - feat(pvc-autoresizer): add support to add new extraPodLabels ([#208](https://github.com/topolvm/pvc-autoresizer/pull/208)) 23 | 24 | ### Changed 25 | 26 | - appVersion was changed to 0.9.0. 27 | 28 | ### Contributors 29 | 30 | - @tejlopes 31 | 32 | ## [0.8.1] - 2023-05-23 33 | 34 | ### Changed 35 | 36 | - appVersion was changed to 0.8.0. 37 | 38 | ## [0.8.0] - 2023-04-05 39 | 40 | ### Added 41 | - artifacthub ([#181](https://github.com/topolvm/pvc-autoresizer/pull/181)) 42 | - Implement initial-resize-group-by feature ([#176](https://github.com/topolvm/pvc-autoresizer/pull/176)) 43 | - **BREAKING**: Added a mutating webhook, required pvc-autoresizer 0.7.0 or higher. 44 | 45 | ### Changed 46 | - Bump helm/chart-testing-action from 2.3.1 to 2.4.0 ([#186](https://github.com/topolvm/pvc-autoresizer/pull/186)) 47 | - appVersion was changed to 0.7.0. 48 | 49 | ### Fixed 50 | - charts/pvc-autoresizer/CHANGELOG.md: fix the broken link ([#182](https://github.com/topolvm/pvc-autoresizer/pull/182)) 51 | 52 | ### Contributors 53 | - @bells17 54 | - @toshipp 55 | 56 | ## [0.7.0] - 2023-02-10 57 | 58 | ### Added 59 | 60 | - Helm chart | Added controller.podAnnotations to helm chart ([#170](https://github.com/topolvm/pvc-autoresizer/pull/170)) 61 | 62 | ### Changed 63 | 64 | - Bump helm/chart-releaser-action from 1.4.1 to 1.5.0 ([#175](https://github.com/topolvm/pvc-autoresizer/pull/175)) 65 | - appVersion was changed to 0.6.1. 66 | 67 | ### Contributors 68 | 69 | - @jcortejoso 70 | 71 | ## [0.6.1] - 2023-01-12 72 | 73 | ### Changed 74 | 75 | - appVersion was changed to 0.6.0. 76 | 77 | ## [0.6.0] - 2022-12-07 78 | 79 | ### Added 80 | 81 | - add podMonitor ([#149](https://github.com/topolvm/pvc-autoresizer/pull/149)) 82 | 83 | ### Contributors 84 | 85 | - @mweibel 86 | 87 | ## [0.5.0] - 2022-08-19 88 | 89 | ### Changed 90 | - appVersion was changed to 0.5.0. 91 | 92 | ## [0.4.0] - 2022-07-04 93 | 94 | ### Added 95 | 96 | - Add support for namespace allow list (#120) 97 | 98 | ### Changed 99 | 100 | - Bump helm/chart-testing-action from 2.0.1 to 2.2.1 (#126) 101 | - Bump helm/chart-releaser-action from 1.2.1 to 1.4.0 (#128) 102 | 103 | ### Contributors 104 | 105 | - @ryanprobus 106 | 107 | ## [0.3.6] - 2022-04-04 108 | 109 | ### Changed 110 | - appVersion was changed to 0.3.1. 111 | 112 | ## [0.3.5] - 2022-03-04 113 | 114 | ### Changed 115 | - appVersion was changed to 0.3.0. 116 | 117 | ## [0.3.4] - 2022-02-07 118 | 119 | ### Changed 120 | - appVersion was changed to 0.2.3. 121 | 122 | ## [0.3.3] - 2022-01-13 123 | 124 | ### Changed 125 | - appVersion was changed to 0.2.2. 126 | 127 | ## [0.3.2] - 2021-11-01 128 | 129 | ### Changed 130 | - appVersion was changed to 0.2.1. 131 | 132 | ## [0.3.1] - 2021-10-06 133 | 134 | ### Added 135 | - Add nodeSelector and tolerations to chart (#69) 136 | 137 | ### Contributors 138 | - @cmotta2016 139 | 140 | ## [0.3.0] - 2021-09-09 141 | 142 | ### Changed 143 | - Change license to Apache License Version 2.0. (#66) 144 | 145 | ## [0.2.0] - 2021-08-10 146 | - First release. 147 | 148 | [Unreleased]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.10.0...HEAD 149 | [0.10.0]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.9.0...pvc-autoresizer-chart-v0.10.0 150 | [0.9.0]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.8.1...pvc-autoresizer-chart-v0.9.0 151 | [0.8.1]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.8.0...pvc-autoresizer-chart-v0.8.1 152 | [0.8.0]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.7.0...pvc-autoresizer-chart-v0.8.0 153 | [0.7.0]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.6.1...pvc-autoresizer-chart-v0.7.0 154 | [0.6.1]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.6.0...pvc-autoresizer-chart-v0.6.1 155 | [0.6.0]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.5.0...pvc-autoresizer-chart-v0.6.0 156 | [0.5.0]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.4.0...pvc-autoresizer-chart-v0.5.0 157 | [0.4.0]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.3.6...pvc-autoresizer-chart-v0.4.0 158 | [0.3.6]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.3.5...pvc-autoresizer-chart-v0.3.6 159 | [0.3.5]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.3.4...pvc-autoresizer-chart-v0.3.5 160 | [0.3.4]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.3.3...pvc-autoresizer-chart-v0.3.4 161 | [0.3.3]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.3.2...pvc-autoresizer-chart-v0.3.3 162 | [0.3.2]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.3.1...pvc-autoresizer-chart-v0.3.2 163 | [0.3.1]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.3.0...pvc-autoresizer-chart-v0.3.1 164 | [0.3.0]: https://github.com/topolvm/pvc-autoresizer/compare/pvc-autoresizer-chart-v0.2.0...pvc-autoresizer-chart-v0.3.0 165 | [0.2.0]: https://github.com/topolvm/pvc-autoresizer/compare/ee8a31ac32b1ad40f0bace32317aa1eee4a8225c...pvc-autoresizer-chart-v0.2.0 166 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/Chart.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: cert-manager 3 | repository: https://charts.jetstack.io 4 | version: v1.7.0 5 | digest: sha256:2d72eb34ee783a5a3e710366d7c50cdaaf9197613b3904907798e7a13dd39f0f 6 | generated: "2023-02-28T01:17:55.374948496+09:00" 7 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: pvc-autoresizer 3 | description: Auto-resize PersistentVolumeClaim objects based on Prometheus metrics. 4 | home: https://github.com/topolvm/pvc-autoresizer 5 | sources: 6 | - https://github.com/topolvm/pvc-autoresizer 7 | 8 | # A chart can be either an 'application' or a 'library' chart. 9 | # 10 | # Application charts are a collection of templates that can be packaged into versioned archives 11 | # to be deployed. 12 | # 13 | # Library charts provide useful utilities or functions for the chart developer. They're included as 14 | # a dependency of application charts to inject those utilities and functions into the rendering 15 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 16 | type: application 17 | 18 | # This is the chart version. This version number should be incremented each time you make changes 19 | # to the chart and its templates, including the app version. 20 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 21 | version: 0.14.2 22 | 23 | # This is the version number of the application being deployed. This version number should be 24 | # incremented each time you make changes to the application. Versions are not expected to 25 | # follow Semantic Versioning. They should reflect the version the application is using. 26 | # It is recommended to use it with quotes. 27 | appVersion: 0.17.3 28 | 29 | annotations: 30 | artifacthub.io/images: | 31 | - name: pvc-autoresizer 32 | image: ghcr.io/topolvm/pvc-autoresizer:0.17.3 33 | artifacthub.io/license: Apache-2.0 34 | 35 | dependencies: 36 | - name: cert-manager 37 | repository: https://charts.jetstack.io 38 | version: 1.7.0 39 | condition: cert-manager.enabled 40 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/README.md: -------------------------------------------------------------------------------- 1 | # pvc-autoresizer Helm Chart 2 | 3 | ## How to use pvc-autoresizer Helm repository 4 | 5 | You need to add this repository to your Helm repositories: 6 | 7 | ```sh 8 | helm repo add pvc-autoresizer https://topolvm.github.io/pvc-autoresizer 9 | helm repo update 10 | ``` 11 | 12 | ## Quick start 13 | 14 | ### Installing the Chart 15 | 16 | To install the chart with the release name `pvc-autoresizer` using a dedicated namespace(recommended): 17 | 18 | ```sh 19 | helm install --create-namespace --namespace pvc-autoresizer pvc-autoresizer pvc-autoresizer/pvc-autoresizer 20 | ``` 21 | 22 | Specify parameters using `--set key=value[,key=value]` argument to `helm install`. 23 | 24 | Alternatively a YAML file that specifies the values for the parameters can be provided like this: 25 | 26 | ```sh 27 | helm upgrade --create-namespace --namespace pvc-autoresizer -i pvc-autoresizer -f values.yaml pvc-autoresizer/pvc-autoresizer 28 | ``` 29 | 30 | ## Values 31 | 32 | | Key | Type | Default | Description | 33 | |-----|------|---------|-------------| 34 | | cert-manager.enabled | bool | `false` | Install cert-manager together. # ref: https://cert-manager.io/docs/installation/helm/#installing-with-helm | 35 | | controller.affinity | object | `{}` | Affinity for controller deployment. | 36 | | controller.annotations | object | `{}` | Annotations to be added to controller deployment. | 37 | | controller.args.additionalArgs | list | `[]` | Specify additional args. | 38 | | controller.args.interval | string | `"10s"` | Specify interval to monitor pvc capacity. Used as "--interval" option | 39 | | controller.args.namespaces | list | `[]` | Specify namespaces to control the pvcs of. Empty for all namespaces. Used as "--namespaces" option | 40 | | controller.args.prometheusURL | string | `"http://prometheus-prometheus-oper-prometheus.prometheus.svc:9090"` | Specify Prometheus URL to query volume stats. Used as "--prometheus-url" option | 41 | | controller.args.useK8sMetricsApi | bool | `false` | Use Kubernetes metrics API instead of Prometheus. Used as "--use-k8s-metrics-api" option | 42 | | controller.nodeSelector | object | `{}` | Map of key-value pairs for scheduling pods on specific nodes. | 43 | | controller.podAnnotations | object | `{}` | Annotations to be added to controller pods. | 44 | | controller.podLabels | object | `{}` | Pod labels to be added to controller pods. | 45 | | controller.podSecurityContext | object | `{}` | Security Context to be applied to the controller pods. | 46 | | controller.priorityClassName | string | `""` | Priority class name to be applied to the controller pods. | 47 | | controller.replicas | int | `1` | Specify the number of replicas of the controller Pod. | 48 | | controller.resources | object | `{"requests":{"cpu":"100m","memory":"20Mi"}}` | Specify resources. | 49 | | controller.securityContext | object | `{}` | Security Context to be applied to the controller container within controller pods. | 50 | | controller.terminationGracePeriodSeconds | string | `nil` | Specify terminationGracePeriodSeconds. | 51 | | controller.tolerations | object | `{}` | Ensure pods are not scheduled on inappropriate nodes. | 52 | | image.pullPolicy | string | `nil` | pvc-autoresizer image pullPolicy. | 53 | | image.repository | string | `"ghcr.io/topolvm/pvc-autoresizer"` | pvc-autoresizer image repository to use. | 54 | | image.tag | string | `{{ .Chart.AppVersion }}` | pvc-autoresizer image tag to use. | 55 | | podMonitor | object | `{"additionalLabels":{},"enabled":false,"interval":"","metricRelabelings":[],"namespace":"","relabelings":[],"scheme":"http","scrapeTimeout":""}` | deploy a PodMonitor. This is not tested in CI so make sure to test it yourself. | 56 | | podMonitor.additionalLabels | object | `{}` | Additional labels that can be used so PodMonitor will be discovered by Prometheus. | 57 | | podMonitor.enabled | bool | `false` | If true, creates a Prometheus Operator PodMonitor. | 58 | | podMonitor.interval | string | `""` | Interval that Prometheus scrapes metrics. | 59 | | podMonitor.metricRelabelings | list | `[]` | MetricRelabelConfigs to apply to samples before ingestion. | 60 | | podMonitor.namespace | string | `""` | Namespace which Prometheus is running in. | 61 | | podMonitor.relabelings | list | `[]` | RelabelConfigs to apply to samples before scraping. | 62 | | podMonitor.scheme | string | `"http"` | Scheme to use for scraping. | 63 | | podMonitor.scrapeTimeout | string | `""` | The timeout after which the scrape is ended | 64 | | serviceAccount.automountServiceAccountToken | bool | `true` | Controls the automatic mounting of ServiceAccount API credentials. | 65 | | serviceAccount.enabled | bool | `true` | Creates a ServiceAccount for the controller deployment. | 66 | | webhook.caBundle | string | `nil` | Specify the certificate to be used for AdmissionWebhook. | 67 | | webhook.certificate.dnsDomain | string | `"cluster.local"` | Cluster DNS domain (required for requesting TLS certificates). | 68 | | webhook.certificate.generate | bool | `false` | Creates a self-signed certificate for 10 years. Once the validity period has expired, simply delete the controller secret and execute helm upgrade. | 69 | | webhook.existingCertManagerIssuer | object | `{}` | Specify the cert-manager issuer to be used for AdmissionWebhook. | 70 | | webhook.pvcMutatingWebhook.enabled | bool | `true` | Enable PVC MutatingWebhook. | 71 | 72 | ## Generate Manifests 73 | 74 | You can use the `helm template` command to render manifests. 75 | 76 | ```sh 77 | helm template --namespace pvc-autoresizer pvc-autoresizer pvc-autoresizer/pvc-autoresizer 78 | ``` 79 | 80 | ## Update README 81 | 82 | The `README.md` for this chart is generated by [helm-docs](https://github.com/norwoodj/helm-docs). 83 | To update the README, edit the `README.md.gotmpl` and generate README like below. 84 | 85 | ```console 86 | # path to topolvm repository root 87 | $ make setup 88 | $ make generate-helm-docs 89 | ``` 90 | 91 | ## Release Chart 92 | 93 | pvc-autoresizer Helm Chart will be released independently. 94 | This will prevent the pvc-autoresizer version from going up just by modifying the Helm Chart. 95 | 96 | You must change the version of [Chart.yaml](./Chart.yaml) when making changes to the Helm Chart. 97 | CI fails with lint error when creating a Pull Request without changing the version of [Chart.yaml](./Chart.yaml). 98 | 99 | When you release the Helm Chart, manually run the GitHub Actions workflow for the release. 100 | 101 | https://github.com/topolvm/pvc-autoresizer/actions/workflows/helm-release.yaml 102 | 103 | When you run workflow, [helm/chart-releaser-action](https://github.com/helm/chart-releaser-action) will automatically create a GitHub Release. 104 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/README.md.gotmpl: -------------------------------------------------------------------------------- 1 | # pvc-autoresizer Helm Chart 2 | 3 | ## How to use pvc-autoresizer Helm repository 4 | 5 | You need to add this repository to your Helm repositories: 6 | 7 | ```sh 8 | helm repo add pvc-autoresizer https://topolvm.github.io/pvc-autoresizer 9 | helm repo update 10 | ``` 11 | 12 | ## Quick start 13 | 14 | ### Installing the Chart 15 | 16 | To install the chart with the release name `pvc-autoresizer` using a dedicated namespace(recommended): 17 | 18 | ```sh 19 | helm install --create-namespace --namespace pvc-autoresizer pvc-autoresizer pvc-autoresizer/pvc-autoresizer 20 | ``` 21 | 22 | Specify parameters using `--set key=value[,key=value]` argument to `helm install`. 23 | 24 | Alternatively a YAML file that specifies the values for the parameters can be provided like this: 25 | 26 | ```sh 27 | helm upgrade --create-namespace --namespace pvc-autoresizer -i pvc-autoresizer -f values.yaml pvc-autoresizer/pvc-autoresizer 28 | ``` 29 | 30 | {{ template "chart.valuesSection" . }} 31 | 32 | ## Generate Manifests 33 | 34 | You can use the `helm template` command to render manifests. 35 | 36 | ```sh 37 | helm template --namespace pvc-autoresizer pvc-autoresizer pvc-autoresizer/pvc-autoresizer 38 | ``` 39 | 40 | ## Update README 41 | 42 | The `README.md` for this chart is generated by [helm-docs](https://github.com/norwoodj/helm-docs). 43 | To update the README, edit the `README.md.gotmpl` and generate README like below. 44 | 45 | ```console 46 | # path to topolvm repository root 47 | $ make setup 48 | $ make generate-helm-docs 49 | ``` 50 | 51 | ## Release Chart 52 | 53 | pvc-autoresizer Helm Chart will be released independently. 54 | This will prevent the pvc-autoresizer version from going up just by modifying the Helm Chart. 55 | 56 | You must change the version of [Chart.yaml](./Chart.yaml) when making changes to the Helm Chart. 57 | CI fails with lint error when creating a Pull Request without changing the version of [Chart.yaml](./Chart.yaml). 58 | 59 | When you release the Helm Chart, manually run the GitHub Actions workflow for the release. 60 | 61 | https://github.com/topolvm/pvc-autoresizer/actions/workflows/helm-release.yaml 62 | 63 | When you run workflow, [helm/chart-releaser-action](https://github.com/helm/chart-releaser-action) will automatically create a GitHub Release. 64 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/templates/NOTES.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topolvm/pvc-autoresizer/fdd778ff382d3f9a47e32206a3472e5fa8a02e06/charts/pvc-autoresizer/templates/NOTES.txt -------------------------------------------------------------------------------- /charts/pvc-autoresizer/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "pvc-autoresizer.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 "pvc-autoresizer.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 "pvc-autoresizer.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "pvc-autoresizer.labels" -}} 37 | helm.sh/chart: {{ include "pvc-autoresizer.chart" . }} 38 | {{ include "pvc-autoresizer.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "pvc-autoresizer.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "pvc-autoresizer.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "pvc-autoresizer.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "pvc-autoresizer.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | 64 | {{/* 65 | Generate certificates for webhook 66 | */}} 67 | {{- define "pvc-autoresizer.webhookCerts" -}} 68 | {{- if .Values.webhook.certificate.generate }} 69 | {{- $serviceName := printf "%s-controller" (include "pvc-autoresizer.fullname" .) -}} 70 | {{- $secret := lookup "v1" "Secret" .Release.Namespace $serviceName -}} 71 | {{- if $secret -}} 72 | caCert: {{ index $secret.data "ca.crt" }} 73 | clientCert: {{ index $secret.data "tls.crt" }} 74 | clientKey: {{ index $secret.data "tls.key" }} 75 | {{- else -}} 76 | {{- $altNames := list (printf "%s.%s" $serviceName .Release.Namespace) (printf "%s.%s.svc" $serviceName .Release.Namespace) (printf "%s.%s.svc.%s" $serviceName .Release.Namespace .Values.webhook.certificate.dnsDomain) -}} 77 | {{- $ca := genCA "pvc-autoresizer-ca" 3650 -}} 78 | {{- $cert := genSignedCert $serviceName nil $altNames 3650 $ca -}} 79 | caCert: {{ $ca.Cert | b64enc }} 80 | clientCert: {{ $cert.Cert | b64enc }} 81 | clientKey: {{ $cert.Key | b64enc }} 82 | {{- end -}} 83 | {{- end -}} 84 | {{- end -}} 85 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/templates/controller/certificate.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.webhook.pvcMutatingWebhook.enabled }} 2 | {{- if not .Values.webhook.caBundle }} 3 | {{- if not .Values.webhook.certificate.generate }} 4 | {{- if not .Values.webhook.existingCertManagerIssuer }} 5 | # Generate a CA Certificate used to sign certificates for the webhook 6 | apiVersion: cert-manager.io/v1 7 | kind: Certificate 8 | metadata: 9 | name: {{ template "pvc-autoresizer.fullname" . }}-webhook-ca 10 | namespace: {{ .Release.Namespace }} 11 | labels: 12 | {{- include "pvc-autoresizer.labels" . | nindent 4 }} 13 | spec: 14 | secretName: {{ template "pvc-autoresizer.fullname" . }}-webhook-ca 15 | duration: 87600h # 10y 16 | issuerRef: 17 | group: cert-manager.io 18 | kind: Issuer 19 | name: {{ template "pvc-autoresizer.fullname" . }}-webhook-selfsign 20 | commonName: ca.webhook.pvc-autoresizer 21 | isCA: true 22 | usages: 23 | - digital signature 24 | - key encipherment 25 | - cert sign 26 | {{- end }} 27 | --- 28 | # Finally, generate a serving certificate for the webhook to use 29 | apiVersion: cert-manager.io/v1 30 | kind: Certificate 31 | metadata: 32 | name: {{ template "pvc-autoresizer.fullname" . }}-controller 33 | namespace: {{ .Release.Namespace }} 34 | labels: 35 | {{- include "pvc-autoresizer.labels" . | nindent 4 }} 36 | spec: 37 | secretName: {{ template "pvc-autoresizer.fullname" . }}-controller 38 | duration: 8760h # 1y 39 | issuerRef: 40 | {{- with .Values.webhook.existingCertManagerIssuer }} 41 | {{- toYaml . | nindent 4 -}} 42 | {{- else }} 43 | group: cert-manager.io 44 | kind: Issuer 45 | name: {{ template "pvc-autoresizer.fullname" . }}-webhook-ca 46 | {{- end }} 47 | dnsNames: 48 | - {{ template "pvc-autoresizer.fullname" . }}-controller 49 | - {{ template "pvc-autoresizer.fullname" . }}-controller.{{ .Release.Namespace }} 50 | - {{ template "pvc-autoresizer.fullname" . }}-controller.{{ .Release.Namespace }}.svc 51 | usages: 52 | - digital signature 53 | - key encipherment 54 | - server auth 55 | - client auth 56 | {{- end }} 57 | {{- end }} 58 | {{- end }} 59 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/templates/controller/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ template "pvc-autoresizer.fullname" . }}-controller-storageclasses 5 | labels: 6 | {{- include "pvc-autoresizer.labels" . | nindent 4 }} 7 | rules: 8 | - apiGroups: 9 | - storage.k8s.io 10 | resources: 11 | - storageclasses 12 | verbs: 13 | - get 14 | - list 15 | - watch 16 | --- 17 | apiVersion: rbac.authorization.k8s.io/v1 18 | kind: ClusterRole 19 | metadata: 20 | name: {{ template "pvc-autoresizer.fullname" . }}-controller 21 | labels: 22 | {{- include "pvc-autoresizer.labels" . | nindent 4 }} 23 | rules: 24 | - apiGroups: 25 | - "" 26 | resources: 27 | - events 28 | verbs: 29 | - create 30 | - get 31 | - list 32 | - watch 33 | - apiGroups: 34 | - "" 35 | resources: 36 | - persistentvolumeclaims 37 | verbs: 38 | - get 39 | - list 40 | - watch 41 | - patch 42 | - update 43 | {{- if .Values.controller.args.useK8sMetricsApi }} 44 | - apiGroups: 45 | - "" 46 | resources: 47 | - nodes 48 | verbs: 49 | - get 50 | - list 51 | - watch 52 | - apiGroups: 53 | - "" 54 | resources: 55 | - "nodes/proxy" 56 | verbs: 57 | - get 58 | - list 59 | - watch 60 | {{- end }} 61 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/templates/controller/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: {{ template "pvc-autoresizer.fullname" . }}-controller-storageclasses 5 | labels: 6 | {{- include "pvc-autoresizer.labels" . | nindent 4 }} 7 | roleRef: 8 | apiGroup: rbac.authorization.k8s.io 9 | kind: ClusterRole 10 | name: {{ template "pvc-autoresizer.fullname" . }}-controller-storageclasses 11 | subjects: 12 | - kind: ServiceAccount 13 | name: {{ template "pvc-autoresizer.fullname" . }}-controller 14 | namespace: {{ .Release.Namespace }} 15 | 16 | {{- if not .Values.controller.args.namespaces }} 17 | --- 18 | apiVersion: rbac.authorization.k8s.io/v1 19 | kind: ClusterRoleBinding 20 | metadata: 21 | name: {{ template "pvc-autoresizer.fullname" . }}-controller 22 | labels: 23 | {{- include "pvc-autoresizer.labels" . | nindent 4 }} 24 | roleRef: 25 | apiGroup: rbac.authorization.k8s.io 26 | kind: ClusterRole 27 | name: {{ template "pvc-autoresizer.fullname" . }}-controller 28 | subjects: 29 | - kind: ServiceAccount 30 | name: {{ template "pvc-autoresizer.fullname" . }}-controller 31 | namespace: {{ .Release.Namespace }} 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/templates/controller/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "pvc-autoresizer.fullname" . }}-controller 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | {{- include "pvc-autoresizer.labels" . | nindent 4 }} 8 | {{- if .Values.controller.annotations}} 9 | annotations: 10 | {{- toYaml .Values.controller.annotations | nindent 4 }} 11 | {{- end }} 12 | spec: 13 | selector: 14 | matchLabels: 15 | {{- include "pvc-autoresizer.selectorLabels" . | nindent 6 }} 16 | replicas: {{ .Values.controller.replicas }} 17 | template: 18 | metadata: 19 | labels: 20 | {{- include "pvc-autoresizer.labels" . | nindent 8 }} 21 | {{- with .Values.controller.podLabels }} 22 | {{- toYaml . | nindent 8 }} 23 | {{- end }} 24 | {{- if .Values.controller.podAnnotations}} 25 | annotations: 26 | {{- toYaml .Values.controller.podAnnotations | nindent 8 }} 27 | {{- end }} 28 | spec: 29 | serviceAccountName: {{ template "pvc-autoresizer.fullname" . }}-controller 30 | {{- with .Values.controller.terminationGracePeriodSeconds }} 31 | terminationGracePeriodSeconds: {{ . }} 32 | {{- end }} 33 | containers: 34 | - name: pvc-autoresizer 35 | command: 36 | - /pvc-autoresizer 37 | args: 38 | - --prometheus-url={{ .Values.controller.args.prometheusURL }} 39 | - --interval={{ .Values.controller.args.interval }} 40 | {{- if .Values.controller.args.useK8sMetricsApi }} 41 | - --use-k8s-metrics-api={{ .Values.controller.args.useK8sMetricsApi }} 42 | {{- end }} 43 | {{- if .Values.controller.args.namespaces }} 44 | - --namespaces={{ join "," .Values.controller.args.namespaces }} 45 | {{- end }} 46 | {{- with .Values.controller.args.additionalArgs -}} 47 | {{ toYaml . | nindent 12 }} 48 | {{- end }} 49 | {{- if not .Values.webhook.pvcMutatingWebhook.enabled }} 50 | - --pvc-mutating-webhook-enabled=false 51 | {{- end}} 52 | image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}" 53 | {{- with .Values.image.pullPolicy }} 54 | imagePullPolicy: {{ . }} 55 | {{- end }} 56 | {{- with .Values.controller.resources }} 57 | resources: {{ toYaml . | nindent 12 }} 58 | {{- end }} 59 | ports: 60 | - containerPort: 9443 61 | name: webhook 62 | protocol: TCP 63 | - name: metrics 64 | containerPort: 8080 65 | protocol: TCP 66 | - name: health 67 | containerPort: 8081 68 | protocol: TCP 69 | readinessProbe: 70 | httpGet: 71 | path: /readyz 72 | port: health 73 | livenessProbe: 74 | httpGet: 75 | path: /healthz 76 | port: health 77 | {{- if .Values.webhook.pvcMutatingWebhook.enabled }} 78 | volumeMounts: 79 | - name: certs 80 | mountPath: /certs 81 | {{- end }} 82 | securityContext: 83 | {{- toYaml .Values.controller.securityContext | nindent 12 }} 84 | {{- with .Values.controller.nodeSelector }} 85 | nodeSelector: 86 | {{- toYaml . | nindent 8 }} 87 | {{- end }} 88 | {{- with .Values.controller.tolerations }} 89 | tolerations: 90 | {{- toYaml . | nindent 8 }} 91 | {{- end }} 92 | {{- if .Values.webhook.pvcMutatingWebhook.enabled }} 93 | volumes: 94 | - name: certs 95 | secret: 96 | defaultMode: 420 97 | secretName: {{ template "pvc-autoresizer.fullname" . }}-controller 98 | {{- end }} 99 | securityContext: 100 | {{- toYaml .Values.controller.podSecurityContext | nindent 8 }} 101 | {{- with .Values.controller.affinity }} 102 | affinity: 103 | {{- toYaml . | nindent 8 }} 104 | {{- end }} 105 | {{- with .Values.controller.priorityClassName }} 106 | priorityClassName: {{ . }} 107 | {{- end }} 108 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/templates/controller/issuer.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.webhook.pvcMutatingWebhook.enabled }} 2 | {{- if not .Values.webhook.caBundle }} 3 | {{- if not .Values.webhook.existingCertManagerIssuer }} 4 | {{- if not .Values.webhook.certificate.generate }} 5 | # Create a selfsigned Issuer, in order to create a root CA certificate for 6 | # signing webhook serving certificates 7 | apiVersion: cert-manager.io/v1 8 | kind: Issuer 9 | metadata: 10 | name: {{ template "pvc-autoresizer.fullname" . }}-webhook-selfsign 11 | namespace: {{ .Release.Namespace }} 12 | labels: 13 | {{- include "pvc-autoresizer.labels" . | nindent 4 }} 14 | spec: 15 | selfSigned: {} 16 | --- 17 | # Create an Issuer that uses the above generated CA certificate to issue certs 18 | apiVersion: cert-manager.io/v1 19 | kind: Issuer 20 | metadata: 21 | name: {{ template "pvc-autoresizer.fullname" . }}-webhook-ca 22 | namespace: {{ .Release.Namespace }} 23 | labels: 24 | {{- include "pvc-autoresizer.labels" . | nindent 4 }} 25 | spec: 26 | ca: 27 | secretName: {{ template "pvc-autoresizer.fullname" . }}-webhook-ca 28 | {{- end }} 29 | {{- end }} 30 | {{- end }} 31 | {{- end }} 32 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/templates/controller/mutatingwebhookconfiguration.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.webhook.pvcMutatingWebhook.enabled }} 2 | {{- $tls := fromYaml ( include "pvc-autoresizer.webhookCerts" . ) }} 3 | --- 4 | apiVersion: admissionregistration.k8s.io/v1 5 | kind: MutatingWebhookConfiguration 6 | metadata: 7 | {{- if and (not .Values.webhook.caBundle) (not .Values.webhook.certificate.generate) }} 8 | annotations: 9 | cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ template "pvc-autoresizer.fullname" . }}-controller 10 | {{- end }} 11 | labels: 12 | {{- include "pvc-autoresizer.labels" . | nindent 4 }} 13 | name: '{{ template "pvc-autoresizer.fullname" . }}-mutating-webhook-configuration' 14 | webhooks: 15 | - admissionReviewVersions: 16 | - v1 17 | clientConfig: 18 | {{- if .Values.webhook.caBundle }} 19 | caBundle: {{ .Values.webhook.caBundle }} 20 | {{- else if .Values.webhook.certificate.generate }} 21 | caBundle: {{ $tls.caCert }} 22 | {{- end }} 23 | service: 24 | name: '{{ template "pvc-autoresizer.fullname" . }}-controller' 25 | namespace: '{{ .Release.Namespace }}' 26 | path: /pvc/mutate 27 | failurePolicy: Fail 28 | name: mpersistentvolumeclaim.topolvm.io 29 | rules: 30 | - apiGroups: 31 | - "" 32 | apiVersions: 33 | - v1 34 | operations: 35 | - CREATE 36 | resources: 37 | - persistentvolumeclaims 38 | sideEffects: None 39 | 40 | {{- if .Values.webhook.certificate.generate }} 41 | --- 42 | apiVersion: v1 43 | kind: Secret 44 | metadata: 45 | name: {{ template "pvc-autoresizer.fullname" . }}-controller 46 | namespace: {{ .Release.Namespace }} 47 | labels: 48 | {{- include "pvc-autoresizer.labels" . | nindent 4 }} 49 | type: kubernetes.io/tls 50 | data: 51 | ca.crt: {{ $tls.caCert }} 52 | tls.crt: {{ $tls.clientCert }} 53 | tls.key: {{ $tls.clientKey }} 54 | {{- end }} 55 | {{- end }} 56 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/templates/controller/podmonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.podMonitor.enabled -}} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: PodMonitor 4 | metadata: 5 | name: {{ template "pvc-autoresizer.fullname" . }} 6 | {{- if .Values.podMonitor.namespace }} 7 | namespace: {{ .Values.podMonitor.namespace }} 8 | {{- end }} 9 | labels: 10 | {{- include "pvc-autoresizer.labels" . | nindent 4 }} 11 | {{- with .Values.podMonitor.additionalLabels }} 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | spec: 15 | jobLabel: {{ .Release.Name }} 16 | namespaceSelector: 17 | matchNames: 18 | - {{ .Release.Namespace }} 19 | selector: 20 | matchLabels: 21 | {{- include "pvc-autoresizer.selectorLabels" . | nindent 6 }} 22 | podMetricsEndpoints: 23 | - port: metrics 24 | path: /metrics 25 | {{- if .Values.podMonitor.scheme }} 26 | scheme: {{ .Values.podMonitor.scheme }} 27 | {{- end }} 28 | {{- with .Values.podMonitor.interval }} 29 | interval: {{ . }} 30 | {{- end }} 31 | {{- with .Values.podMonitor.scrapeTimeout }} 32 | scrapeTimeout: {{ . }} 33 | {{- end }} 34 | {{- with .Values.podMonitor.relabelings }} 35 | relabelings: 36 | {{- toYaml . | nindent 6 }} 37 | {{- end }} 38 | {{- with .Values.podMonitor.metricRelabelings }} 39 | metricRelabelings: 40 | {{- toYaml . | nindent 6 }} 41 | {{- end }} 42 | {{- end -}} 43 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/templates/controller/role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: {{ template "pvc-autoresizer.fullname" . }}-leader-election 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "pvc-autoresizer.labels" . | nindent 4 }} 9 | rules: 10 | - apiGroups: 11 | - "" 12 | - coordination.k8s.io 13 | resources: 14 | - configmaps 15 | - leases 16 | verbs: 17 | - get 18 | - list 19 | - watch 20 | - create 21 | - update 22 | - patch 23 | - delete 24 | - apiGroups: 25 | - "" 26 | resources: 27 | - events 28 | verbs: 29 | - create 30 | - patch 31 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/templates/controller/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: {{ template "pvc-autoresizer.fullname" . }}-leader-election 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | {{- include "pvc-autoresizer.labels" . | nindent 4 }} 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: Role 11 | name: {{ template "pvc-autoresizer.fullname" . }}-leader-election 12 | subjects: 13 | - kind: ServiceAccount 14 | name: {{ template "pvc-autoresizer.fullname" . }}-controller 15 | namespace: {{ .Release.Namespace }} 16 | 17 | {{- range .Values.controller.args.namespaces }} 18 | --- 19 | apiVersion: rbac.authorization.k8s.io/v1 20 | kind: RoleBinding 21 | metadata: 22 | name: {{ template "pvc-autoresizer.fullname" $ }}-controller 23 | namespace: {{ . }} 24 | labels: 25 | {{- include "pvc-autoresizer.labels" $ | nindent 4 }} 26 | roleRef: 27 | apiGroup: rbac.authorization.k8s.io 28 | kind: ClusterRole 29 | name: {{ template "pvc-autoresizer.fullname" $ }}-controller 30 | subjects: 31 | - kind: ServiceAccount 32 | name: {{ template "pvc-autoresizer.fullname" $ }}-controller 33 | namespace: {{ $.Release.Namespace }} 34 | {{- end }} 35 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/templates/controller/service.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.webhook.pvcMutatingWebhook.enabled }} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ template "pvc-autoresizer.fullname" . }}-controller 6 | namespace: {{ .Release.Namespace }} 7 | spec: 8 | ports: 9 | - port: 443 10 | targetPort: 9443 11 | selector: 12 | {{- include "pvc-autoresizer.selectorLabels" . | nindent 4 }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/templates/controller/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.enabled }} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ template "pvc-autoresizer.fullname" . }}-controller 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "pvc-autoresizer.labels" . | nindent 4 }} 9 | automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} 10 | {{- end }} 11 | -------------------------------------------------------------------------------- /charts/pvc-autoresizer/values.yaml: -------------------------------------------------------------------------------- 1 | image: 2 | # image.repository -- pvc-autoresizer image repository to use. 3 | repository: ghcr.io/topolvm/pvc-autoresizer 4 | 5 | # image.tag -- pvc-autoresizer image tag to use. 6 | # @default -- `{{ .Chart.AppVersion }}` 7 | tag: # 0.17.3 8 | 9 | # image.pullPolicy -- pvc-autoresizer image pullPolicy. 10 | pullPolicy: # Always 11 | 12 | controller: 13 | # controller.replicas -- Specify the number of replicas of the controller Pod. 14 | replicas: 1 15 | 16 | args: 17 | # controller.args.useK8sMetricsApi -- Use Kubernetes metrics API instead of Prometheus. 18 | # Used as "--use-k8s-metrics-api" option 19 | useK8sMetricsApi: false 20 | 21 | # controller.args.prometheusURL -- Specify Prometheus URL to query volume stats. 22 | # Used as "--prometheus-url" option 23 | prometheusURL: http://prometheus-prometheus-oper-prometheus.prometheus.svc:9090 24 | 25 | # controller.args.namespaces -- Specify namespaces to control the pvcs of. Empty for all namespaces. 26 | # Used as "--namespaces" option 27 | namespaces: [] 28 | 29 | # controller.args.interval -- Specify interval to monitor pvc capacity. 30 | # Used as "--interval" option 31 | interval: 10s 32 | 33 | # controller.args.additionalArgs -- Specify additional args. 34 | additionalArgs: [] 35 | 36 | # controller.resources -- Specify resources. 37 | resources: 38 | requests: 39 | cpu: 100m 40 | memory: 20Mi 41 | 42 | # controller.annotations -- Annotations to be added to controller deployment. 43 | annotations: {} 44 | 45 | # controller.podLabels -- Pod labels to be added to controller pods. 46 | podLabels: {} 47 | 48 | # controller.podAnnotations -- Annotations to be added to controller pods. 49 | podAnnotations: {} 50 | 51 | # controller.podSecurityContext -- Security Context to be applied to the controller pods. 52 | podSecurityContext: {} 53 | 54 | # controller.securityContext -- Security Context to be applied to the controller container within controller pods. 55 | securityContext: {} 56 | # allowPrivilegeEscalation: false 57 | # capabilities: 58 | # drop: 59 | # - ALL 60 | # readOnlyRootFilesystem: true 61 | # runAsNonRoot: true 62 | # runAsUser: 1000 63 | # seccompProfile: 64 | # type: RuntimeDefault 65 | 66 | # controller.terminationGracePeriodSeconds -- Specify terminationGracePeriodSeconds. 67 | terminationGracePeriodSeconds: # 10 68 | 69 | # controller.tolerations -- Ensure pods are not scheduled on inappropriate nodes. 70 | tolerations: {} 71 | 72 | # controller.nodeSelector -- Map of key-value pairs for scheduling pods on specific nodes. 73 | nodeSelector: {} 74 | 75 | # controller.affinity -- Affinity for controller deployment. 76 | affinity: {} 77 | # podAffinity: 78 | # requiredDuringSchedulingIgnoredDuringExecution: 79 | # - labelSelector: 80 | # matchExpressions: 81 | # - key: app.kubernetes.io/name 82 | # operator: In 83 | # values: 84 | # - pvc-autoresizer 85 | # topologyKey: topology.kubernetes.io/zone 86 | # podAntiAffinity: 87 | # preferredDuringSchedulingIgnoredDuringExecution: 88 | # - weight: 100 89 | # podAffinityTerm: 90 | # labelSelector: 91 | # matchExpressions: 92 | # - key: app.kubernetes.io/name 93 | # operator: In 94 | # values: 95 | # - pvc-autoresizer 96 | # topologyKey: topology.kubernetes.io/zone 97 | 98 | # controller.priorityClassName -- Priority class name to be applied to the controller pods. 99 | priorityClassName: "" 100 | # priorityClassName: system-cluster-critical 101 | 102 | # -- deploy a PodMonitor. This is not tested in CI so make sure to test it yourself. 103 | podMonitor: 104 | # podMonitor.enabled -- If true, creates a Prometheus Operator PodMonitor. 105 | enabled: false 106 | # podMonitor.scheme -- Scheme to use for scraping. 107 | scheme: http 108 | # podMonitor.interval -- Interval that Prometheus scrapes metrics. 109 | interval: "" 110 | # podMonitor.scrapeTimeout -- The timeout after which the scrape is ended 111 | scrapeTimeout: "" 112 | # podMonitor.namespace -- Namespace which Prometheus is running in. 113 | namespace: "" 114 | # podMonitor.relabelings -- RelabelConfigs to apply to samples before scraping. 115 | relabelings: [] 116 | # - sourceLabels: [__meta_kubernetes_service_label_cluster] 117 | # targetLabel: cluster 118 | # regex: (.*) 119 | # replacement: ${1} 120 | # action: replace 121 | 122 | # podMonitor.metricRelabelings -- MetricRelabelConfigs to apply to samples before ingestion. 123 | metricRelabelings: [] 124 | # - sourceLabels: [__meta_kubernetes_service_label_cluster] 125 | # targetLabel: cluster 126 | # regex: (.*) 127 | # replacement: ${1} 128 | # action: replace 129 | 130 | # podMonitor.additionalLabels -- Additional labels that can be used so PodMonitor will be discovered by Prometheus. 131 | additionalLabels: {} 132 | 133 | webhook: 134 | certificate: 135 | # webhook.certificate.generate -- Creates a self-signed certificate for 10 years. Once the validity period has expired, simply delete the controller secret and execute helm upgrade. 136 | generate: false 137 | # webhook.certificate.dnsDomain -- Cluster DNS domain (required for requesting TLS certificates). 138 | dnsDomain: cluster.local 139 | # webhook.caBundle -- Specify the certificate to be used for AdmissionWebhook. 140 | caBundle: # Base64-encoded, PEM-encoded CA certificate that signs the server certificate. 141 | # webhook.existingCertManagerIssuer -- Specify the cert-manager issuer to be used for AdmissionWebhook. 142 | existingCertManagerIssuer: {} 143 | # group: cert-manager.io 144 | # kind: Issuer 145 | # name: webhook-issuer 146 | pvcMutatingWebhook: 147 | # webhook.pvcMutatingWebhook.enabled -- Enable PVC MutatingWebhook. 148 | enabled: true 149 | 150 | cert-manager: 151 | # cert-manager.enabled -- Install cert-manager together. 152 | ## ref: https://cert-manager.io/docs/installation/helm/#installing-with-helm 153 | enabled: false 154 | 155 | serviceAccount: 156 | # serviceAccount.enabled -- Creates a ServiceAccount for the controller deployment. 157 | enabled: true 158 | # serviceAccount.automountServiceAccountToken -- Controls the automatic mounting of ServiceAccount API credentials. 159 | automountServiceAccountToken: true 160 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "github.com/spf13/cobra" 10 | _ "k8s.io/client-go/plugin/pkg/client/auth" 11 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 12 | //+kubebuilder:scaffold:imports 13 | ) 14 | 15 | var config struct { 16 | certDir string 17 | webhookAddr string 18 | metricsAddr string 19 | healthAddr string 20 | namespaces []string 21 | watchInterval time.Duration 22 | prometheusURL string 23 | useK8sMetricsApi bool 24 | skipAnnotation bool 25 | development bool 26 | zapOpts zap.Options 27 | pvcMutatingWebhookEnabled bool 28 | } 29 | 30 | // rootCmd represents the base command when called without any subcommands 31 | var rootCmd = &cobra.Command{ 32 | Use: "pvc-autoresizer", 33 | Short: "PVC Autoresizer", 34 | Long: `pvc-autoresizer is an automatic volume resizer that edits PVCs if they have less than the specified ` + 35 | `amount of free filesystem capacity.`, 36 | RunE: func(cmd *cobra.Command, args []string) error { 37 | cmd.SilenceUsage = true 38 | return subMain() 39 | }, 40 | } 41 | 42 | func main() { 43 | if err := rootCmd.Execute(); err != nil { 44 | fmt.Println(err) 45 | os.Exit(1) 46 | } 47 | } 48 | 49 | func init() { 50 | fs := rootCmd.Flags() 51 | fs.StringVar(&config.certDir, "cert-dir", "/certs", "webhook certificate directory") 52 | fs.StringVar(&config.webhookAddr, "webhook-addr", ":9443", "Listen address for the webhook endpoint") 53 | fs.StringVar(&config.metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 54 | fs.StringVar(&config.healthAddr, "health-addr", ":8081", "The address of health/readiness probes.") 55 | fs.StringSliceVar(&config.namespaces, "namespaces", []string{}, 56 | "Namespaces to resize PersistentVolumeClaims within. Empty for all namespaces.") 57 | fs.DurationVar(&config.watchInterval, "interval", 1*time.Minute, "Interval to monitor pvc capacity.") 58 | fs.StringVar(&config.prometheusURL, "prometheus-url", "", "Prometheus URL to query volume stats.") 59 | fs.BoolVar(&config.useK8sMetricsApi, "use-k8s-metrics-api", false, "Use Kubernetes metrics API instead of Prometheus") 60 | fs.BoolVar(&config.skipAnnotation, "no-annotation-check", false, "Skip annotation check for StorageClass") 61 | fs.BoolVar(&config.development, "development", false, "Use development logger config") 62 | fs.BoolVar(&config.pvcMutatingWebhookEnabled, "pvc-mutating-webhook-enabled", true, 63 | "Enable the pvc mutating webhook endpoint") 64 | 65 | goflags := flag.NewFlagSet("zap", flag.ExitOnError) 66 | config.zapOpts.BindFlags(goflags) 67 | fs.AddGoFlagSet(goflags) 68 | } 69 | -------------------------------------------------------------------------------- /cmd/run.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/topolvm/pvc-autoresizer/internal/hooks" 8 | "github.com/topolvm/pvc-autoresizer/internal/runners" 9 | corev1 "k8s.io/api/core/v1" 10 | storagev1 "k8s.io/api/storage/v1" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 13 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 14 | ctrl "sigs.k8s.io/controller-runtime" 15 | "sigs.k8s.io/controller-runtime/pkg/cache" 16 | "sigs.k8s.io/controller-runtime/pkg/client" 17 | "sigs.k8s.io/controller-runtime/pkg/healthz" 18 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 19 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 20 | "sigs.k8s.io/controller-runtime/pkg/webhook" 21 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 22 | //+kubebuilder:scaffold:imports 23 | ) 24 | 25 | var ( 26 | scheme = runtime.NewScheme() 27 | setupLog = ctrl.Log.WithName("setup") 28 | ) 29 | 30 | func init() { 31 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 32 | 33 | utilruntime.Must(corev1.AddToScheme(scheme)) 34 | //+kubebuilder:scaffold:scheme 35 | } 36 | 37 | func subMain() error { 38 | if config.development { 39 | config.zapOpts.Development = true 40 | } 41 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&config.zapOpts))) 42 | 43 | var webhookServer webhook.Server 44 | if config.pvcMutatingWebhookEnabled { 45 | hookHost, portStr, err := net.SplitHostPort(config.webhookAddr) 46 | if err != nil { 47 | setupLog.Error(err, "invalid webhook addr") 48 | return err 49 | } 50 | hookPort, err := net.LookupPort("tcp", portStr) 51 | if err != nil { 52 | setupLog.Error(err, "invalid webhook port") 53 | return err 54 | } 55 | 56 | webhookServer = webhook.NewServer(webhook.Options{ 57 | Host: hookHost, 58 | Port: hookPort, 59 | CertDir: config.certDir, 60 | }) 61 | } 62 | 63 | graceTimeout := 10 * time.Second 64 | 65 | var pvcCacheTarget cache.ByObject 66 | if len(config.namespaces) == 0 { 67 | pvcCacheTarget = cache.ByObject{ 68 | Namespaces: map[string]cache.Config{ 69 | cache.AllNamespaces: {}, 70 | }, 71 | } 72 | } else { 73 | pvcCacheTarget = cache.ByObject{ 74 | Namespaces: map[string]cache.Config{}, 75 | } 76 | for _, ns := range config.namespaces { 77 | pvcCacheTarget.Namespaces[ns] = cache.Config{} 78 | } 79 | } 80 | 81 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 82 | Scheme: scheme, 83 | WebhookServer: webhookServer, 84 | Metrics: metricsserver.Options{ 85 | BindAddress: config.metricsAddr, 86 | }, 87 | Cache: cache.Options{ 88 | ByObject: map[client.Object]cache.ByObject{ 89 | &corev1.PersistentVolumeClaim{}: pvcCacheTarget, 90 | &storagev1.StorageClass{}: {}, 91 | }, 92 | }, 93 | HealthProbeBindAddress: config.healthAddr, 94 | LeaderElection: true, 95 | LeaderElectionID: "49e22f61.topolvm.io", 96 | GracefulShutdownTimeout: &graceTimeout, 97 | }) 98 | if err != nil { 99 | setupLog.Error(err, "unable to start manager") 100 | return err 101 | } 102 | 103 | if err := mgr.AddHealthzCheck("ping", healthz.Ping); err != nil { 104 | return err 105 | } 106 | if err := mgr.AddReadyzCheck("ping", healthz.Ping); err != nil { 107 | return err 108 | } 109 | if config.pvcMutatingWebhookEnabled { 110 | if err := mgr.AddReadyzCheck("webhook", mgr.GetWebhookServer().StartedChecker()); err != nil { 111 | return err 112 | } 113 | } 114 | 115 | var metricsClient runners.MetricsClient 116 | if config.useK8sMetricsApi { 117 | metricsClient, err = runners.NewK8sMetricsApiClient() 118 | } else if config.prometheusURL != "" { 119 | metricsClient, err = runners.NewPrometheusClient(config.prometheusURL) 120 | } else { 121 | setupLog.Error(err, "enable use-k8s-metrics-api or provide prometheus-url") 122 | return err 123 | } 124 | 125 | if err != nil { 126 | setupLog.Error(err, "unable to initialize metrics client") 127 | return err 128 | } 129 | 130 | if err := runners.SetupIndexer(mgr, config.skipAnnotation); err != nil { 131 | setupLog.Error(err, "unable to initialize pvc autoresizer") 132 | return err 133 | } 134 | 135 | pvcAutoresizer := runners.NewPVCAutoresizer(metricsClient, mgr.GetClient(), 136 | ctrl.Log.WithName("pvc-autoresizer"), 137 | config.watchInterval, mgr.GetEventRecorderFor("pvc-autoresizer")) 138 | if err := mgr.Add(pvcAutoresizer); err != nil { 139 | setupLog.Error(err, "unable to add autoresier to manager") 140 | return err 141 | } 142 | 143 | if config.pvcMutatingWebhookEnabled { 144 | dec := admission.NewDecoder(scheme) 145 | if err = hooks.SetupPersistentVolumeClaimWebhook(mgr, dec, ctrl.Log.WithName("hooks")); err != nil { 146 | setupLog.Error(err, "unable to create PersistentVolumeClaim webhook") 147 | return err 148 | } 149 | } 150 | 151 | //+kubebuilder:scaffold:builder 152 | 153 | setupLog.Info("starting manager") 154 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 155 | setupLog.Error(err, "problem running manager") 156 | return err 157 | } 158 | return nil 159 | } 160 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: controller 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - events 11 | verbs: 12 | - create 13 | - get 14 | - list 15 | - watch 16 | - apiGroups: 17 | - "" 18 | resources: 19 | - persistentvolumeclaims 20 | verbs: 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - storage.k8s.io 28 | resources: 29 | - storageclasses 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | -------------------------------------------------------------------------------- /config/webhook/manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: MutatingWebhookConfiguration 4 | metadata: 5 | name: mutating-webhook-configuration 6 | webhooks: 7 | - admissionReviewVersions: 8 | - v1 9 | clientConfig: 10 | service: 11 | name: webhook-service 12 | namespace: system 13 | path: /pvc/mutate 14 | failurePolicy: Fail 15 | name: mpersistentvolumeclaim.topolvm.io 16 | rules: 17 | - apiGroups: 18 | - "" 19 | apiVersions: 20 | - v1 21 | operations: 22 | - CREATE 23 | resources: 24 | - persistentvolumeclaims 25 | sideEffects: None 26 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package pvcautoresizer 2 | 3 | // AutoResizeEnabledKey is the key of flag that enables pvc-autoresizer. 4 | const AutoResizeEnabledKey = "resize.topolvm.io/enabled" 5 | 6 | // ResizeThresholdAnnotation is the key of resize threshold. 7 | const ResizeThresholdAnnotation = "resize.topolvm.io/threshold" 8 | 9 | // ResizeInodesThresholdAnnotation is the key of resize threshold for inodes. 10 | const ResizeInodesThresholdAnnotation = "resize.topolvm.io/inodes-threshold" 11 | 12 | // ResizeIncreaseAnnotation is the key of amount increased. 13 | const ResizeIncreaseAnnotation = "resize.topolvm.io/increase" 14 | 15 | // StorageLimitAnnotation is the key of storage limit value 16 | const StorageLimitAnnotation = "resize.topolvm.io/storage_limit" 17 | 18 | // PreviousCapacityBytesAnnotation is the key of previous volume capacity. 19 | const PreviousCapacityBytesAnnotation = "resize.topolvm.io/pre_capacity_bytes" 20 | 21 | // InitialResizeGroupByAnnotation is the key of the initial-resize group by. 22 | const InitialResizeGroupByAnnotation = "resize.topolvm.io/initial-resize-group-by" 23 | 24 | // DefaultThreshold is the default value of ResizeThresholdAnnotation. 25 | const DefaultThreshold = "10%" 26 | 27 | // DefaultInodesThreshold is the default value of ResizeInodesThresholdAnnotation. 28 | const DefaultInodesThreshold = "10%" 29 | 30 | // DefaultIncrease is the default value of ResizeIncreaseAnnotation. 31 | const DefaultIncrease = "10%" 32 | -------------------------------------------------------------------------------- /ct.yaml: -------------------------------------------------------------------------------- 1 | # This file is the config file for helm/chart-testing 2 | target-branch: main 3 | validate-maintainers: false 4 | check-version-increment: false 5 | chart-repos: 6 | - jetstack=https://charts.jetstack.io 7 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # Design notes 2 | 3 | ## Motivation 4 | 5 | Resizable persistent volumes (PVs) make operations of applications easier when 6 | it is difficult to predict the storage size required for the applications. 7 | 8 | That said, it can be a hassle to resize them one by one manually if there are 9 | a lot of such persistent volumes. 10 | 11 | ## Goal 12 | 13 | - Automate resizing of persistent volumes using filesystem usage metrics. 14 | - Users can configure resizing parameters for each PVC. 15 | - Users can enable this feature only for specific StorageClasses. 16 | 17 | ## Target 18 | 19 | - CSI drivers which support [`VolumeExpansion`](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#csi-volume-expansion) (ex. TopoLVM, Ceph-CSI). 20 | - Only Filesystem volume mode (not Block). 21 | 22 | ## Architecture 23 | 24 | ```mermaid 25 | %%{init:{'theme': 'default'}}%% 26 | 27 | flowchart LR 28 | 29 | style Architecture fill:#FFFFFF 30 | subgraph Architecture 31 | 32 | classDef storageComponent fill:#FFFF00 33 | classDef component fill:#ADD8E6 34 | 35 | style PVCA fill:#90EE90 36 | style storage fill:#FFFFE0 37 | style Node fill-opacity:0 38 | 39 | CD[CSI Driver]:::storageComponent 40 | PVC[PersistentVolumeClaim]:::storageComponent 41 | SC[StorageClass]:::storageComponent 42 | PVCA[pvc-autoresizer] 43 | 44 | kubelet:::component 45 | Prometheus:::component 46 | kube-apiserver:::component 47 | 48 | subgraph Node 49 | kubelet 50 | storage 51 | end 52 | 53 | subgraph kube-apiserver 54 | SC --- PVC 55 | end 56 | 57 | CD -- watch PVC --> PVC 58 | CD -- expand volume --> storage[(Storage)] 59 | Prometheus -- get metrics --> kubelet 60 | 61 | PVCA -- 1. get target PVCs --> PVC 62 | PVCA -- 2. get SCs --> SC 63 | PVCA -- 3. get metrics of storage capacity --> Prometheus 64 | PVCA -- 4. expand storage capacity --> PVC 65 | 66 | end 67 | ``` 68 | 69 | ### How pvc-autoresizer works 70 | 71 | To expand PVC, pvc-autoresizer works as follows: 72 | 73 | 1. Get target PVC information from `kube-apiserver`. 74 | 2. Get StorageClass related to the PVC from `kube-apiserver`. 75 | 3. Get filesystem usage metrics from Prometheus that scrapes the information from `kubelet`. 76 | 4. Expand PVC storage request size if PVC has less than the specified amount of free filesystem capacity. 77 | 78 | ### Details 79 | 80 | To enable pvc-autoresizer, prepare StorageClass as follows: 81 | 82 | ```yaml 83 | kind: StorageClass 84 | apiVersion: storage.k8s.io/v1 85 | metadata: 86 | name: topolvm-provisioner 87 | annotations: 88 | resize.topolvm.io/enabled: "true" 89 | provisioner: topolvm.io 90 | parameters: 91 | "csi.storage.k8s.io/fstype": "xfs" 92 | volumeBindingMode: WaitForFirstConsumer 93 | allowVolumeExpansion: true 94 | ``` 95 | 96 | - To allow automatic resizing, StorageClass must have `resize.topolvm.io/enabled` annotation. 97 | - `allowVolumeExpansion` should be `true`. 98 | 99 | In addition to the StorageClass, prepare PVC as follows: 100 | 101 | ```yaml 102 | kind: PersistentVolumeClaim 103 | apiVersion: v1 104 | metadata: 105 | name: topolvm-pvc 106 | annotations: 107 | resize.topolvm.io/threshold: 20% 108 | resize.topolvm.io/inodes-threshold: 20% 109 | resize.topolvm.io/increase: 20Gi 110 | resize.topolvm.io/storage_limit: 100Gi 111 | spec: 112 | accessModes: 113 | - ReadWriteOnce 114 | volumeMode: Filesystem 115 | resources: 116 | requests: 117 | storage: 30Gi 118 | storageClassName: topolvm-provisioner 119 | ``` 120 | 121 | - `spec.storageClassName` should be put above StorageClass (in this case "topolvm-provisioner"). 122 | - To allow automatic resizing, PVC must have `resize.topolvm.io/storage_limit` annotation. 123 | - pvc-autoresizer increases PVC's `spec.resources.requests.storage` up to the given limits. 124 | - The threshold of free space is given by `resize.topolvm.io/threshold` annotation. 125 | - The threshold of free inodes is given by `resize.topolvm.io/inodes-threshold` annotation. 126 | - The amount of increased size can be specified by `resize.topolvm.io/increase` annotation. 127 | - The value of the annotations can be a ratio like `20%` or a value like `10Gi`. 128 | - The default value for both threshold and amount is `10%`. 129 | - `spec.volumeMode` must be Filesystem (default is Filesystem). 130 | -------------------------------------------------------------------------------- /docs/maintenance.md: -------------------------------------------------------------------------------- 1 | Maintenance guide 2 | ================= 3 | 4 | How to change the supported Kubernetes minor versions 5 | ------------------------------------------- 6 | 7 | pvc-autoresizer depends on some Kubernetes repositories like `k8s.io/client-go` and should support 3 consecutive Kubernetes versions at a time. 8 | 9 | Issues and PRs related to the last upgrade task also help you understand how to upgrade the supported versions, 10 | so checking them together(e.g https://github.com/topolvm/pvc-autoresizer/pull/85) with this guide is recommended when you do this task. 11 | 12 | ### Upgrade procedure 13 | 14 | We should write down in the github issue of this task what are the important changes and the required actions to manage incompatibilities if exist. 15 | The format is up to you. 16 | 17 | Basically, we should pay attention to breaking changes and security fixes first. 18 | 19 | #### TopoLVM 20 | 21 | Choose the [TopoLVM](https://github.com/topolvm/topolvm/releases) version that supports target Kubernetes version. 22 | 23 | To change the version, edit `versions.mk`. If TopoLVM which supports the target Kubernetes version has not released yet, you can specify the commit hash instead of the tag. 24 | 25 | #### Kubernetes 26 | 27 | Choose the next version and check the [release note](https://kubernetes.io/docs/setup/release/notes/). e.g. 1.17, 1.18, 1.19 -> 1.18, 1.19, 1.20 28 | 29 | To change the version, edit the following files. 30 | 31 | - `.github/workflows/e2e.yaml` 32 | - `README.md` 33 | - `versions.mk` 34 | 35 | We should also update go.mod by the following commands. Please note that Kubernetes v1 corresponds with v0 for the release tags. For example, v1.17.2 corresponds with the v0.17.2 tag. 36 | 37 | ```bash 38 | $ VERSION= 39 | $ go get k8s.io/api@v${VERSION} k8s.io/apimachinery@v${VERSION} k8s.io/client-go@v${VERSION} 40 | ``` 41 | 42 | Read the [`controller-runtime`'s release note](https://github.com/kubernetes-sigs/controller-runtime/releases), and update to the newest version that is compatible with all supported kubernetes versions. If there are breaking changes, we should decide how to manage these changes. 43 | 44 | ``` 45 | $ VERSION= 46 | $ go get sigs.k8s.io/controller-runtime@v${VERSION} 47 | ``` 48 | 49 | Read the [`controller-tools`'s release note](https://github.com/kubernetes-sigs/controller-tools/releases), and update to the newest version that is compatible with all supported kubernetes versions. If there are breaking changes, we should decide how to manage these changes. To change the version, edit `versions.mk`. 50 | 51 | #### Go 52 | 53 | Choose the same version of Go [used by the latest Kubernetes](https://github.com/kubernetes/kubernetes/blob/master/go.mod) supported by pvc-autoresizer. 54 | 55 | Edit the following files. 56 | 57 | - `go.mod` 58 | - `Dockerfile` 59 | 60 | #### Depending tools 61 | 62 | The following tools do not depend on other software, use latest versions. 63 | To change their versions, edit `versions.mk`. 64 | - [chart-testing](https://github.com/helm/chart-testing/releases) 65 | - [golangci-lint](https://github.com/golangci/golangci-lint/releases) 66 | - [helm-docs](https://github.com/norwoodj/helm-docs/releases) 67 | - [helm](https://github.com/helm/helm/releases) 68 | - [kube-prometheus](https://github.com/prometheus-operator/kube-prometheus/releases) 69 | 70 | #### Depending modules 71 | 72 | Please tidy up the dependencies. 73 | 74 | ```bash 75 | $ go mod tidy 76 | ``` 77 | 78 | Regenerate manifests using new controller-tools. 79 | 80 | ```bash 81 | $ make setup 82 | $ make generate 83 | ``` 84 | 85 | #### Final check 86 | 87 | `git grep `, `git grep image:`, `git grep -i VERSION` and looking `versions.mk` might help to avoid overlooking necessary changes. 88 | -------------------------------------------------------------------------------- /docs/proposals/resize-when-creating-by-group.md: -------------------------------------------------------------------------------- 1 | # Expand the PVC's initial capacity based on the largest capacity in the specified PVCs. 2 | 3 | ## Summary 4 | 5 | Expand the PVC's initial capacity based on the largest capacity in specified PVCs. 6 | 7 | ### Motivation 8 | 9 | One use case for pvc-autoresizer is automatically expanding a PV/PVC for multiple Pod/PV pairs, such as the following example. 10 | In such a cluster, if a Pod/PV fails and a PV/PVC is rebuilt by the restore process, a PV is created based on the PVC template. Even if a PV of the same size as the others is actually needed, a PV of the template's capacity is created. As a result, the restore process stops due to insufficient capacity. 11 | 12 | To solve this problem, we are planning to add a new feature to create a PV/PVC with sufficient capacity when PVCs of the same group already exist. 13 | 14 | #### Example of the behavior we want to resolve 15 | 16 | The following is a behavior of actual restore failures of [MOCO](https://github.com/cybozu-go/moco). 17 | 18 | 1. MOCO creates a StatefulSet to manage a MySQL cluster. In addition, a PVC is consumed per one instance(pod). Let assume the initial volume size, which is written in VolumeClaimTemplate, is 100GiB. 19 | 1. These PVCs have expanded to 200GiB according to the growth of DB using pvc-autoresizer. 20 | 1. One instance dies. Then a new pod and the corresponding instance is created to fill a vacancy. 21 | 1. The new PVC, which is consumed by the new instance is created. In this case, the size of the new PVC is not 200GiB, but 100GiB. 22 | 1. The data clone to the existing instances to the new instance might fail due to the lack of volume size. 23 | 24 | ### Goals 25 | 26 | - Provide the ability to increase the PVC's initial capacity based on the largest capacity in specified PVCs. 27 | - The PVCs for which the initial capacity increase is enabled can be selected by users. 28 | - Some application checks the PV capacity at the beginning of the process. It should be able to handle such cases. 29 | 30 | ## Proposal 31 | 32 | ### Rules for grouping & initial resize 33 | 34 | - If a PVC being created has a `resize.topolvm.io/initial-resize-group-by` annotation and the label specified in the annotation exists, existing PVCs with matching label values are considered to be in the same group. 35 | - If no PVCs are in the same group, or if the capacity of creating PVC is larger than any existing PVCs in the same group, the capacity of the original PVC is used as is. 36 | 37 | Examples are given below. 38 | 39 | ```yaml 40 | ### existing PVCs (excerpted) 41 | kind: PersistentVolumeClaim 42 | metadata: 43 | name: pvc-x-1 44 | labels: 45 | label-foobar: group-x 46 | annotations: 47 | resize.topolvm.io/initial-resize-group-by: label-foobar 48 | spec: 49 | resources: 50 | requests: 51 | storage: 20Gi 52 | 53 | kind: PersistentVolumeClaim 54 | metadata: 55 | name: pvc-x-2 56 | labels: 57 | label-foobar: group-x 58 | annotations: 59 | resize.topolvm.io/initial-resize-group-by: label-foobar 60 | spec: 61 | resources: 62 | requests: 63 | storage: 16Gi 64 | 65 | kind: PersistentVolumeClaim 66 | metadata: 67 | name: pvc-y-1 68 | labels: 69 | label-foobar: group-y 70 | annotations: 71 | resize.topolvm.io/initial-resize-group-by: label-foobar 72 | spec: 73 | resources: 74 | requests: 75 | storage: 30Gi 76 | ``` 77 | 78 | When creating the following new PVC, `pvc-x-1` and `pvc-x-2` with `label-foobar: group-x` are considered to be in the same group, and `pvc-y-1` is not. Therefore, the PVC is created with **20Gi** based on `pvc-x-1`, which has the largest capacity in the group. 79 | 80 | ```yaml 81 | kind: PersistentVolumeClaim 82 | metadata: 83 | name: pvc-x-3 84 | labels: 85 | label-foobar: group-x 86 | annotations: 87 | resize.topolvm.io/initial-resize-group-by: label-foobar 88 | spec: 89 | resources: 90 | requests: 91 | storage: 10Gi 92 | ``` 93 | 94 | When creating the following new PVC, `pvc-y-1` with `label-foobar: group-y` is in the same group. However, since the new PVC's size(50Gi) is larger than the existing one(30Gi), the PVC is created with **50Gi**. 95 | 96 | ```yaml 97 | kind: PersistentVolumeClaim 98 | metadata: 99 | name: pvc-y-2 100 | labels: 101 | label-foobar: group-y 102 | annotations: 103 | resize.topolvm.io/initial-resize-group-by: label-foobar 104 | spec: 105 | resources: 106 | requests: 107 | storage: 50Gi 108 | ``` 109 | 110 | ### How to use 111 | 112 | - Add `resize.topolvm.io/initial-resize-group-by` annotation to the PVC template to which you want to apply the feature. 113 | - In most cases, annotations and labels are not added to PVCs that have already been created based on a PVC template. In such cases, it is necessary to manually add `resize.topolvm.io/initial-resize-group-by` annotation to existing PVCs in addition to the PVC template. 114 | 115 | ## Design details 116 | 117 | ### Option A) use mutating webhook for PVC 118 | 119 | Pros: 120 | - A restore program that checks the free capacity of the volume will not cause problems. 121 | - We do not need to worry about expansion failure after scheduling PVs because new PVC is created with sufficient capacity. 122 | 123 | Cons: 124 | - In order to provide webhook features, additional configuration of mutating webhook and svc is required besides the development of the program. 125 | 126 | ### Option B) use reconcile loop for PVC 127 | 128 | Pros: 129 | - pvc-autoresizer already has a custom controller that monitors PVCs, so the functionality may be achieved with a few modifications. 130 | 131 | Cons: 132 | - A restore program that performs a free space check of the volume when using PVC may cause an error due to insufficient space before expanding PV/PVC. 133 | - In TopoLVM, there may be cases in which PVs created cannot be expanded due to insufficient node capacity. 134 | 135 | ### Decision outcome 136 | 137 | We adopt Option A. We have already found out that MOCO checks the free space of PVCs before restore process. Therefore, the restore does not work well with Option B. Other programs may behave in the same way. 138 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | This directory contains manifests to run `pvc-autoresizer` in a demonstration environment. 4 | 5 | ## Setup TopoLVM 6 | 7 | Deploy [TopoLVM] on [kind] as follows: 8 | 9 | - https://github.com/topolvm/topolvm/tree/master/example 10 | 11 | ## Setup Prometheus 12 | 13 | Deploy [Prometheus Operator] via [helm] as follows: 14 | 15 | ``` 16 | helm repo add prometheus-community https://prometheus-community.github.io/helm-charts 17 | helm repo update 18 | helm install prometheus prometheus-community/kube-prometheus-stack --create-namespace --namespace=prometheus 19 | ``` 20 | 21 | ## Deploy pvc-autoresizer 22 | 23 | Load the container image of `pvc-autoresizer` to [kind] as follows: 24 | 25 | ``` 26 | make image 27 | kind load docker-image --name topolvm-example ghcr.io/topolvm/pvc-autoresizer:devel 28 | ``` 29 | 30 | Deploy `pvc-autoresizer`: 31 | 32 | ``` 33 | helm repo add pvc-autoresizer https://topolvm.github.io/pvc-autoresizer/ 34 | helm repo update 35 | helm install pvc-autoresizer pvc-autoresizer/pvc-autoresizer --create-namespace --namespace=pvc-autoresizer --set "controller.args.prometheusURL=http://prometheus-kube-prometheus-prometheus.prometheus.svc:9090" 36 | ``` 37 | 38 | ## Enable auto-resizing 39 | 40 | Annotating a storage class enables the automatic resizing of PVCs it is associated with: 41 | 42 | ``` 43 | kubectl annotate storageclass topolvm-provisioner resize.topolvm.io/enabled=true 44 | ``` 45 | 46 | ## Deploy a Pod 47 | 48 | Deploy a Pod and PVC to be resized: 49 | 50 | ``` 51 | kubectl apply -f podpvc.yaml 52 | ``` 53 | 54 | ## Resizing PVC 55 | 56 | Enter into the target pod: 57 | 58 | ``` 59 | kubectl exec -it example-pod bash 60 | ``` 61 | 62 | Make sure current volume usage: 63 | 64 | ``` 65 | root@example-pod:/# df -h /test1 66 | Filesystem Size Used Avail Use% Mounted on 67 | /dev/topolvm/8ad1c617-e572-4d0d-b4e8-d66e5a572df9 1014M 34M 981M 4% /test1 68 | ``` 69 | 70 | Create a file that take up 90% of the volume: 71 | 72 | ``` 73 | fallocate -l 900M /test1/test.txt 74 | ``` 75 | 76 | Make sure current volume usage again: 77 | 78 | ``` 79 | root@example-pod:/# df -h /test1 80 | Filesystem Size Used Avail Use% Mounted on 81 | /dev/topolvm/8ad1c617-e572-4d0d-b4e8-d66e5a572df9 1014M 934M 81M 93% /test1 82 | ``` 83 | 84 | After a few minutes, the volume will be resized to 2GiB: 85 | 86 | ``` 87 | root@example-pod:/# df -h /test1 88 | Filesystem Size Used Avail Use% Mounted on 89 | /dev/topolvm/8ad1c617-e572-4d0d-b4e8-d66e5a572df9 2.0G 935M 1.1G 46% /test1 90 | ``` 91 | 92 | [TopoLVM]: https://github.com/topolvm/topolvm/ 93 | [Prometheus Operator]: https://github.com/prometheus-operator/prometheus-operator 94 | [Helm]: https://helm.sh/ 95 | [kind]: https://github.com/kubernetes-sigs/kind 96 | -------------------------------------------------------------------------------- /example/podpvc.yaml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolumeClaim 2 | apiVersion: v1 3 | metadata: 4 | name: resizable-pvc 5 | annotations: 6 | resize.topolvm.io/storage_limit: 10Gi 7 | resize.topolvm.io/threshold: 20% 8 | resize.topolvm.io/increase: 2Gi 9 | spec: 10 | accessModes: 11 | - ReadWriteOnce 12 | resources: 13 | requests: 14 | storage: 1Gi 15 | storageClassName: topolvm-provisioner 16 | --- 17 | apiVersion: v1 18 | kind: Pod 19 | metadata: 20 | name: example-pod 21 | labels: 22 | app.kubernetes.io/name: example-pod 23 | spec: 24 | containers: 25 | - name: ubuntu 26 | image: ubuntu:22.04 27 | command: 28 | - bash 29 | - -c 30 | - | 31 | sleep inf & 32 | trap "kill -SIGTERM $!" SIGTERM 33 | wait $! 34 | exit 35 | volumeMounts: 36 | - mountPath: /test1 37 | name: my-volume 38 | volumes: 39 | - name: my-volume 40 | persistentVolumeClaim: 41 | claimName: resizable-pvc 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/topolvm/pvc-autoresizer 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/go-logr/logr v1.4.2 7 | github.com/onsi/ginkgo/v2 v2.22.0 8 | github.com/onsi/gomega v1.36.1 9 | github.com/prometheus/client_golang v1.19.1 10 | github.com/prometheus/client_model v0.6.1 11 | github.com/prometheus/common v0.55.0 12 | github.com/spf13/cobra v1.8.1 13 | golang.org/x/sync v0.12.0 14 | k8s.io/api v0.32.2 15 | k8s.io/apimachinery v0.32.2 16 | k8s.io/client-go v0.32.2 17 | sigs.k8s.io/controller-runtime v0.20.3 18 | sigs.k8s.io/yaml v1.4.0 19 | ) 20 | 21 | require ( 22 | github.com/beorn7/perks v1.0.1 // indirect 23 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 24 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 25 | github.com/emicklei/go-restful/v3 v3.11.1 // indirect 26 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 27 | github.com/fsnotify/fsnotify v1.7.0 // indirect 28 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 29 | github.com/go-logr/zapr v1.3.0 // indirect 30 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 31 | github.com/go-openapi/jsonreference v0.20.4 // indirect 32 | github.com/go-openapi/swag v0.23.0 // indirect 33 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 34 | github.com/gogo/protobuf v1.3.2 // indirect 35 | github.com/golang/protobuf v1.5.4 // indirect 36 | github.com/google/btree v1.1.3 // indirect 37 | github.com/google/gnostic-models v0.6.8 // indirect 38 | github.com/google/go-cmp v0.6.0 // indirect 39 | github.com/google/gofuzz v1.2.0 // indirect 40 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect 41 | github.com/google/uuid v1.6.0 // indirect 42 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 43 | github.com/josharian/intern v1.0.0 // indirect 44 | github.com/json-iterator/go v1.1.12 // indirect 45 | github.com/mailru/easyjson v0.7.7 // indirect 46 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 47 | github.com/modern-go/reflect2 v1.0.2 // indirect 48 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 49 | github.com/pkg/errors v0.9.1 // indirect 50 | github.com/prometheus/procfs v0.15.1 // indirect 51 | github.com/spf13/pflag v1.0.5 // indirect 52 | github.com/x448/float16 v0.8.4 // indirect 53 | go.uber.org/multierr v1.11.0 // indirect 54 | go.uber.org/zap v1.27.0 // indirect 55 | golang.org/x/net v0.38.0 // indirect 56 | golang.org/x/oauth2 v0.23.0 // indirect 57 | golang.org/x/sys v0.31.0 // indirect 58 | golang.org/x/term v0.30.0 // indirect 59 | golang.org/x/text v0.23.0 // indirect 60 | golang.org/x/time v0.7.0 // indirect 61 | golang.org/x/tools v0.26.0 // indirect 62 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 63 | google.golang.org/protobuf v1.35.1 // indirect 64 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 65 | gopkg.in/inf.v0 v0.9.1 // indirect 66 | gopkg.in/yaml.v3 v3.0.1 // indirect 67 | k8s.io/apiextensions-apiserver v0.32.1 // indirect 68 | k8s.io/klog/v2 v2.130.1 // indirect 69 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 70 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 71 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 72 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 73 | ) 74 | -------------------------------------------------------------------------------- /internal/hooks/persistentvolumeclaim.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/go-logr/logr" 10 | pvcautoresizer "github.com/topolvm/pvc-autoresizer" 11 | "github.com/topolvm/pvc-autoresizer/internal/runners" 12 | admissionv1 "k8s.io/api/admission/v1" 13 | corev1 "k8s.io/api/core/v1" 14 | "k8s.io/apimachinery/pkg/labels" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | "sigs.k8s.io/controller-runtime/pkg/manager" 17 | "sigs.k8s.io/controller-runtime/pkg/webhook" 18 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 19 | ) 20 | 21 | //+kubebuilder:webhook:path=/pvc/mutate,mutating=true,failurePolicy=fail,sideEffects=None,groups="",resources=persistentvolumeclaims,verbs=create,versions=v1,name=mpersistentvolumeclaim.topolvm.io,admissionReviewVersions={v1} 22 | 23 | type persistentVolumeClaimMutator struct { 24 | apiReader client.Reader 25 | dec admission.Decoder 26 | log logr.Logger 27 | } 28 | 29 | var _ admission.Handler = &persistentVolumeClaimMutator{} 30 | 31 | func (m *persistentVolumeClaimMutator) Handle(ctx context.Context, req admission.Request) admission.Response { 32 | if req.Operation != admissionv1.Create { 33 | return admission.Allowed("not a Create request") 34 | } 35 | pvc := &corev1.PersistentVolumeClaim{} 36 | if err := m.dec.Decode(req, pvc); err != nil { 37 | return admission.Errored(http.StatusBadRequest, err) 38 | } 39 | groupLabelKey, ok := pvc.Annotations[pvcautoresizer.InitialResizeGroupByAnnotation] 40 | if !ok || groupLabelKey == "" { 41 | return admission.Allowed("annotation not set") 42 | } 43 | group, ok := pvc.Labels[groupLabelKey] 44 | if !ok || group == "" { 45 | return admission.Errored(http.StatusBadRequest, fmt.Errorf("no value is set to the label key %s", groupLabelKey)) 46 | } 47 | 48 | storageLimit, err := runners.PvcStorageLimit(pvc) 49 | if err != nil { 50 | return admission.Errored(http.StatusInternalServerError, err) 51 | } 52 | if storageLimit.IsZero() { 53 | return admission.Allowed("ignore the PVC because it has no storage limit annotation") 54 | } 55 | 56 | pvcList := &corev1.PersistentVolumeClaimList{} 57 | err = m.apiReader.List(ctx, pvcList, &client.ListOptions{ 58 | Namespace: pvc.Namespace, 59 | LabelSelector: labels.SelectorFromSet(map[string]string{groupLabelKey: group}), 60 | }) 61 | if err != nil { 62 | return admission.Errored(http.StatusInternalServerError, err) 63 | } 64 | 65 | requestedSize := *pvc.Spec.Resources.Requests.Storage() 66 | newSize := requestedSize 67 | for _, item := range pvcList.Items { 68 | if itemSize := item.Spec.Resources.Requests.Storage(); itemSize.Cmp(newSize) > 0 { 69 | newSize = *itemSize 70 | } 71 | } 72 | if newSize.Cmp(storageLimit) > 0 { 73 | newSize = storageLimit 74 | } 75 | if requestedSize.Cmp(newSize) == 0 { 76 | return admission.Allowed("PVC request storage size unchanged") 77 | } 78 | pvc.Spec.Resources.Requests[corev1.ResourceStorage] = newSize 79 | 80 | m.log.Info("need mutate the PVC size", 81 | "name", pvc.Name, 82 | "namespace", pvc.Namespace, 83 | "from-request", requestedSize.Value(), 84 | "to-request", pvc.Spec.Resources.Requests.Storage().Value(), 85 | ) 86 | data, err := json.Marshal(pvc) 87 | if err != nil { 88 | return admission.Errored(http.StatusInternalServerError, err) 89 | } 90 | return admission.PatchResponseFromRaw(req.Object.Raw, data) 91 | } 92 | 93 | // SetupPersistentVolumeClaimWebhook registers the webhooks for PersistentVolumeClaim 94 | func SetupPersistentVolumeClaimWebhook(mgr manager.Manager, dec admission.Decoder, log logr.Logger) error { 95 | serv := mgr.GetWebhookServer() 96 | m := &persistentVolumeClaimMutator{ 97 | apiReader: mgr.GetAPIReader(), 98 | dec: dec, 99 | log: log, 100 | } 101 | serv.Register("/pvc/mutate", &webhook.Admission{Handler: m}) 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /internal/metrics/kubernetes_client.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | runtimemetrics "sigs.k8s.io/controller-runtime/pkg/metrics" 6 | ) 7 | 8 | // Metrics subsystem and all of the keys used by the metrics client. 9 | const ( 10 | KubernetesClientSubsystem = "kubernetes_client" 11 | KubernetesClientFailTotalKey = "fail_total" 12 | ) 13 | 14 | func init() { 15 | registerKubernetesClientMetrics() 16 | } 17 | 18 | type KubernetesClientFailTotalAdapter struct { 19 | metric prometheus.Counter 20 | } 21 | 22 | func (a *KubernetesClientFailTotalAdapter) Increment() { 23 | a.metric.Inc() 24 | } 25 | 26 | var ( 27 | kubernetesClientFailTotal = prometheus.NewCounter(prometheus.CounterOpts{ 28 | Namespace: MetricsNamespace, 29 | Subsystem: KubernetesClientSubsystem, 30 | Name: KubernetesClientFailTotalKey, 31 | Help: "counter that indicates how many API requests to kube-api server are failed.", 32 | }) 33 | 34 | KubernetesClientFailTotal *KubernetesClientFailTotalAdapter = &KubernetesClientFailTotalAdapter{ 35 | metric: kubernetesClientFailTotal, 36 | } 37 | ) 38 | 39 | func registerKubernetesClientMetrics() { 40 | runtimemetrics.Registry.MustRegister(kubernetesClientFailTotal) 41 | } 42 | -------------------------------------------------------------------------------- /internal/metrics/kubernetes_client_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/prometheus/client_golang/prometheus/testutil" 7 | ) 8 | 9 | func TestKubernetesClientFailTotal(t *testing.T) { 10 | KubernetesClientFailTotal.Increment() 11 | actual := testutil.ToFloat64(kubernetesClientFailTotal) 12 | if actual != float64(1) { 13 | t.Fatalf("value is not %d", 1) 14 | } 15 | 16 | KubernetesClientFailTotal.Increment() 17 | actual = testutil.ToFloat64(kubernetesClientFailTotal) 18 | if actual != float64(2) { 19 | t.Fatalf("value is not %d", 2) 20 | } 21 | 22 | actual2 := testutil.CollectAndCount(kubernetesClientFailTotal) 23 | if actual2 != 1 { 24 | t.Fatalf("the count of metrics is not 1 actual=%d", actual2) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/metrics/mertics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | const MetricsNamespace string = "pvcautoresizer" 4 | -------------------------------------------------------------------------------- /internal/metrics/metrics_client.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | runtimemetrics "sigs.k8s.io/controller-runtime/pkg/metrics" 6 | ) 7 | 8 | // Metrics subsystem and all of the keys used by the metrics client. 9 | const ( 10 | MetricsClientSubsystem = "metrics_client" 11 | MetricsClientFailTotalKey = "fail_total" 12 | ) 13 | 14 | func init() { 15 | registerMetricsClientMetrics() 16 | } 17 | 18 | type metricsClientFailTotalAdapter struct { 19 | metric prometheus.Counter 20 | } 21 | 22 | func (a *metricsClientFailTotalAdapter) Increment() { 23 | a.metric.Inc() 24 | } 25 | 26 | var ( 27 | metricsClientFailTotal = prometheus.NewCounter(prometheus.CounterOpts{ 28 | Namespace: MetricsNamespace, 29 | Subsystem: MetricsClientSubsystem, 30 | Name: MetricsClientFailTotalKey, 31 | Help: "counter that indicates how many API requests to metrics server(e.g. prometheus) are failed.", 32 | }) 33 | 34 | MetricsClientFailTotal *metricsClientFailTotalAdapter = &metricsClientFailTotalAdapter{metric: metricsClientFailTotal} 35 | ) 36 | 37 | func registerMetricsClientMetrics() { 38 | runtimemetrics.Registry.MustRegister(metricsClientFailTotal) 39 | } 40 | -------------------------------------------------------------------------------- /internal/metrics/metrics_client_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/prometheus/client_golang/prometheus/testutil" 8 | ) 9 | 10 | func TestMetricsClientFailTotal(t *testing.T) { 11 | MetricsClientFailTotal.Increment() 12 | actual := testutil.ToFloat64(metricsClientFailTotal.(prometheus.Collector)) 13 | if actual != float64(1) { 14 | t.Fatalf("value is not %d", 1) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/metrics/resizer.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | runtimemetrics "sigs.k8s.io/controller-runtime/pkg/metrics" 6 | ) 7 | 8 | // Metrics subsystem and all of the keys used by the resizer. 9 | const ( 10 | ResizerSuccessResizeTotalKey = "success_resize_total" 11 | ResizerFailedResizeTotalKey = "failed_resize_total" 12 | ResizerLoopSecondsTotalKey = "loop_seconds_total" 13 | ResizerLimitReachedTotalKey = "limit_reached_total" 14 | ) 15 | 16 | func init() { 17 | registerResizerMetrics() 18 | } 19 | 20 | type resizerSuccessResizeTotalAdapter struct { 21 | metric prometheus.CounterVec 22 | } 23 | 24 | func (a *resizerSuccessResizeTotalAdapter) Increment(pvcname string, pvcns string) { 25 | a.metric.With(prometheus.Labels{"persistentvolumeclaim": pvcname, "namespace": pvcns}).Inc() 26 | } 27 | 28 | // SpecifyLabels helps output metrics before the first resize event. 29 | // This method specifies the metric labels and add 0 to the metric value. 30 | func (a *resizerSuccessResizeTotalAdapter) SpecifyLabels(pvcname string, pvcns string) { 31 | a.metric.With(prometheus.Labels{"persistentvolumeclaim": pvcname, "namespace": pvcns}).Add(0) 32 | } 33 | 34 | type resizerFailedResizeTotalAdapter struct { 35 | metric prometheus.CounterVec 36 | } 37 | 38 | func (a *resizerFailedResizeTotalAdapter) Increment(pvcname string, pvcns string) { 39 | a.metric.With(prometheus.Labels{"persistentvolumeclaim": pvcname, "namespace": pvcns}).Inc() 40 | } 41 | 42 | // SpecifyLabels helps output metrics before the first fail event of resize. 43 | // This method specifies the metric labels and add 0 to the metric value. 44 | func (a *resizerFailedResizeTotalAdapter) SpecifyLabels(pvcname string, pvcns string) { 45 | a.metric.With(prometheus.Labels{"persistentvolumeclaim": pvcname, "namespace": pvcns}).Add(0) 46 | } 47 | 48 | type resizerLoopSecondsTotalAdapter struct { 49 | metric prometheus.Counter 50 | } 51 | 52 | func (a *resizerLoopSecondsTotalAdapter) Add(value float64) { 53 | a.metric.Add(value) 54 | } 55 | 56 | type resizerLimitReachedTotalAdapter struct { 57 | metric prometheus.CounterVec 58 | } 59 | 60 | func (a *resizerLimitReachedTotalAdapter) Increment(pvcname string, pvcns string) { 61 | a.metric.With(prometheus.Labels{"persistentvolumeclaim": pvcname, "namespace": pvcns}).Inc() 62 | } 63 | 64 | // SpecifyLabels helps output metrics before the first limit reached event of resize. 65 | // This method specifies the metric labels and add 0 to the metric value. 66 | func (a *resizerLimitReachedTotalAdapter) SpecifyLabels(pvcname string, pvcns string) { 67 | a.metric.With(prometheus.Labels{"persistentvolumeclaim": pvcname, "namespace": pvcns}).Add(0) 68 | } 69 | 70 | var ( 71 | resizerSuccessResizeTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ 72 | Namespace: MetricsNamespace, 73 | Name: ResizerSuccessResizeTotalKey, 74 | Help: "counter that indicates how many volume expansion processing resized succeed.", 75 | }, []string{"persistentvolumeclaim", "namespace"}) 76 | 77 | resizerFailedResizeTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ 78 | Namespace: MetricsNamespace, 79 | Name: ResizerFailedResizeTotalKey, 80 | Help: "counter that indicates how many volume expansion processing resizes fail.", 81 | }, []string{"persistentvolumeclaim", "namespace"}) 82 | 83 | resizerLoopSecondsTotal = prometheus.NewCounter(prometheus.CounterOpts{ 84 | Namespace: MetricsNamespace, 85 | Name: ResizerLoopSecondsTotalKey, 86 | Help: "counter that indicates the sum of seconds spent on volume expansion processing loops.", 87 | }) 88 | 89 | resizerLimitReachedTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ 90 | Namespace: MetricsNamespace, 91 | Name: ResizerLimitReachedTotalKey, 92 | Help: "counter that indicates how many storage limits were reached.", 93 | }, []string{"persistentvolumeclaim", "namespace"}) 94 | 95 | ResizerSuccessResizeTotal *resizerSuccessResizeTotalAdapter = &resizerSuccessResizeTotalAdapter{ 96 | metric: *resizerSuccessResizeTotal, 97 | } 98 | ResizerFailedResizeTotal *resizerFailedResizeTotalAdapter = &resizerFailedResizeTotalAdapter{ 99 | metric: *resizerFailedResizeTotal, 100 | } 101 | ResizerLoopSecondsTotal *resizerLoopSecondsTotalAdapter = &resizerLoopSecondsTotalAdapter{ 102 | metric: resizerLoopSecondsTotal, 103 | } 104 | ResizerLimitReachedTotal *resizerLimitReachedTotalAdapter = &resizerLimitReachedTotalAdapter{ 105 | metric: *resizerLimitReachedTotal, 106 | } 107 | ) 108 | 109 | func registerResizerMetrics() { 110 | runtimemetrics.Registry.MustRegister(resizerSuccessResizeTotal) 111 | runtimemetrics.Registry.MustRegister(resizerFailedResizeTotal) 112 | runtimemetrics.Registry.MustRegister(resizerLoopSecondsTotal) 113 | runtimemetrics.Registry.MustRegister(resizerLimitReachedTotal) 114 | } 115 | -------------------------------------------------------------------------------- /internal/metrics/resizer_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/prometheus/client_golang/prometheus/testutil" 8 | ) 9 | 10 | func TestResizerSuccessResizeTotal(t *testing.T) { 11 | ResizerSuccessResizeTotal.Increment("my-test-pvc", "my-test-namespace") 12 | actual := testutil.ToFloat64(resizerSuccessResizeTotal) 13 | if actual != float64(1) { 14 | t.Fatalf("value is not %d", 1) 15 | } 16 | } 17 | 18 | func TestResizerFailedResizeTotal(t *testing.T) { 19 | ResizerFailedResizeTotal.Increment("my-test-pvc", "my-test-namespace") 20 | actual := testutil.ToFloat64(resizerFailedResizeTotal) 21 | if actual != float64(1) { 22 | t.Fatalf("value is not %d", 1) 23 | } 24 | } 25 | 26 | func TestResizerLoopSecondsTotal(t *testing.T) { 27 | ns := "default" 28 | name := "pvc" 29 | 30 | ResizerLoopSecondsTotal.Add(10) 31 | actual := testutil.ToFloat64(resizerLoopSecondsTotal.(prometheus.Collector)) 32 | if actual != float64(10) { 33 | t.Fatalf("namespace=%s name=%s value is not %d", ns, name, 10) 34 | } 35 | } 36 | 37 | func TestResizerLimitReachedTotal(t *testing.T) { 38 | ResizerLimitReachedTotal.Increment("my-test-pvc", "my-test-namespace") 39 | actual := testutil.ToFloat64(resizerLimitReachedTotal) 40 | if actual != float64(1) { 41 | t.Fatalf("value is not %d", 1) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /internal/runners/fake_client_wrapper.go: -------------------------------------------------------------------------------- 1 | package runners 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | type fakeClientWrapper struct { 11 | client.Client 12 | } 13 | 14 | func NewFakeClientWrapper(c client.Client) client.Client { 15 | return &fakeClientWrapper{ 16 | Client: c, 17 | } 18 | } 19 | 20 | func (c *fakeClientWrapper) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 21 | return errors.New("occurred fake error") 22 | } 23 | -------------------------------------------------------------------------------- /internal/runners/k8s_metrics_api_client.go: -------------------------------------------------------------------------------- 1 | package runners 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "sync" 8 | 9 | dto "github.com/prometheus/client_model/go" 10 | "github.com/prometheus/common/expfmt" 11 | "github.com/topolvm/pvc-autoresizer/internal/metrics" 12 | "golang.org/x/sync/errgroup" 13 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/types" 15 | "k8s.io/client-go/kubernetes" 16 | "k8s.io/client-go/rest" 17 | ) 18 | 19 | // NewK8sMetricsApiClient returns a new k8sMetricsApiClient client 20 | func NewK8sMetricsApiClient() (MetricsClient, error) { 21 | return &k8sMetricsApiClient{}, nil 22 | } 23 | 24 | type k8sMetricsApiClient struct { 25 | } 26 | 27 | func (c *k8sMetricsApiClient) GetMetrics(ctx context.Context) (map[types.NamespacedName]*VolumeStats, error) { 28 | // create a Kubernetes client using in-cluster configuration 29 | config, err := rest.InClusterConfig() 30 | if err != nil { 31 | metrics.MetricsClientFailTotal.Increment() 32 | return nil, err 33 | } 34 | 35 | clientset, err := kubernetes.NewForConfig(config) 36 | if err != nil { 37 | metrics.MetricsClientFailTotal.Increment() 38 | return nil, err 39 | } 40 | 41 | // get a list of nodes and IP addresses 42 | nodes, err := clientset.CoreV1().Nodes().List(ctx, v1.ListOptions{}) 43 | if err != nil { 44 | metrics.MetricsClientFailTotal.Increment() 45 | return nil, err 46 | } 47 | 48 | // create a map to hold PVC usage data 49 | pvcUsage := make(map[types.NamespacedName]*VolumeStats) 50 | var mu sync.Mutex // serialize writes to pvcUsage 51 | 52 | // use an errgroup to query kubelet for PVC usage on each node 53 | eg, ctx := errgroup.WithContext(ctx) 54 | for _, node := range nodes.Items { 55 | nodeName := node.Name 56 | eg.Go(func() error { 57 | nodePVCUsage, err := getPVCUsageFromK8sMetricsAPI(ctx, clientset, nodeName) 58 | if err != nil { 59 | return err 60 | } 61 | mu.Lock() 62 | defer mu.Unlock() 63 | for k, v := range nodePVCUsage { 64 | pvcUsage[k] = v 65 | } 66 | return nil 67 | }) 68 | } 69 | 70 | // wait for all queries to complete and handle any errors 71 | if err := eg.Wait(); err != nil { 72 | metrics.MetricsClientFailTotal.Increment() 73 | return nil, err 74 | } 75 | 76 | return pvcUsage, nil 77 | } 78 | 79 | func getPVCUsageFromK8sMetricsAPI( 80 | ctx context.Context, clientset *kubernetes.Clientset, nodeName string, 81 | ) (map[types.NamespacedName]*VolumeStats, error) { 82 | // make the request to the api /metrics endpoint and handle the response 83 | req := clientset. 84 | CoreV1(). 85 | RESTClient(). 86 | Get(). 87 | Resource("nodes"). 88 | Name(nodeName). 89 | SubResource("proxy"). 90 | Suffix("metrics") 91 | respBody, err := req.DoRaw(ctx) 92 | if err != nil { 93 | return nil, fmt.Errorf("failed to get stats from kubelet on node %s: %w", nodeName, err) 94 | } 95 | parser := expfmt.TextParser{} 96 | metricFamilies, err := parser.TextToMetricFamilies(bytes.NewReader(respBody)) 97 | if err != nil { 98 | return nil, fmt.Errorf("failed to read response body from kubelet on node %s: %w", nodeName, err) 99 | } 100 | 101 | pvcUsage := make(map[types.NamespacedName]*VolumeStats) 102 | 103 | // volumeAvailableQuery 104 | if gauge, ok := metricFamilies[volumeAvailableQuery]; ok { 105 | for _, m := range gauge.Metric { 106 | pvcName, value := parseMetric(m) 107 | pvcUsage[pvcName] = &VolumeStats{} 108 | pvcUsage[pvcName].AvailableBytes = int64(value) 109 | } 110 | } 111 | // volumeCapacityQuery 112 | if gauge, ok := metricFamilies[volumeCapacityQuery]; ok { 113 | for _, m := range gauge.Metric { 114 | pvcName, value := parseMetric(m) 115 | pvcUsage[pvcName].CapacityBytes = int64(value) 116 | } 117 | } 118 | 119 | // inodesAvailableQuery 120 | if gauge, ok := metricFamilies[inodesAvailableQuery]; ok { 121 | for _, m := range gauge.Metric { 122 | pvcName, value := parseMetric(m) 123 | pvcUsage[pvcName].AvailableInodeSize = int64(value) 124 | } 125 | } 126 | 127 | // inodesCapacityQuery 128 | if gauge, ok := metricFamilies[inodesCapacityQuery]; ok { 129 | for _, m := range gauge.Metric { 130 | pvcName, value := parseMetric(m) 131 | pvcUsage[pvcName].CapacityInodeSize = int64(value) 132 | } 133 | } 134 | return pvcUsage, nil 135 | } 136 | 137 | func parseMetric(m *dto.Metric) (pvcName types.NamespacedName, value uint64) { 138 | for _, label := range m.GetLabel() { 139 | if label.GetName() == "namespace" { 140 | pvcName.Namespace = label.GetValue() 141 | } else if label.GetName() == "persistentvolumeclaim" { 142 | pvcName.Name = label.GetValue() 143 | } 144 | } 145 | value = uint64(m.GetGauge().GetValue()) 146 | return pvcName, value 147 | } 148 | -------------------------------------------------------------------------------- /internal/runners/metrics_client.go: -------------------------------------------------------------------------------- 1 | package runners 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/types" 7 | ) 8 | 9 | const ( 10 | volumeAvailableQuery = "kubelet_volume_stats_available_bytes" 11 | volumeCapacityQuery = "kubelet_volume_stats_capacity_bytes" 12 | inodesAvailableQuery = "kubelet_volume_stats_inodes_free" 13 | inodesCapacityQuery = "kubelet_volume_stats_inodes" 14 | ) 15 | 16 | // MetricsClient is an interface for getting metrics 17 | type MetricsClient interface { 18 | // GetMetrics returns volume stats metrics of PVCs 19 | // 20 | // The volume stats consist of available bytes, capacity bytes, available inodes and capacity 21 | // inodes. This method returns volume stats for a PVC only if all four metrics of the PVC was 22 | // retrieved from the metrics source. 23 | GetMetrics(ctx context.Context) (map[types.NamespacedName]*VolumeStats, error) 24 | } 25 | 26 | // VolumeStats is a struct containing metrics used by pvc-autoresizer 27 | type VolumeStats struct { 28 | AvailableBytes int64 29 | CapacityBytes int64 30 | AvailableInodeSize int64 31 | CapacityInodeSize int64 32 | } 33 | -------------------------------------------------------------------------------- /internal/runners/metrics_client_test.go: -------------------------------------------------------------------------------- 1 | package runners 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "sync" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | "k8s.io/apimachinery/pkg/types" 12 | ) 13 | 14 | type prometheusClientMock struct { 15 | stats map[types.NamespacedName]*VolumeStats 16 | mutex sync.Mutex 17 | } 18 | 19 | func (c *prometheusClientMock) GetMetrics(ctx context.Context) (map[types.NamespacedName]*VolumeStats, error) { 20 | c.mutex.Lock() 21 | defer c.mutex.Unlock() 22 | copied := make(map[types.NamespacedName]*VolumeStats) 23 | for k, v := range c.stats { 24 | copied[k] = v 25 | } 26 | return copied, nil 27 | } 28 | 29 | func (c *prometheusClientMock) setResponce(key types.NamespacedName, stats *VolumeStats) { 30 | c.mutex.Lock() 31 | defer c.mutex.Unlock() 32 | if c.stats == nil { 33 | c.stats = make(map[types.NamespacedName]*VolumeStats) 34 | } 35 | c.stats[key] = stats 36 | } 37 | 38 | var _ = Describe("test prometheusClient", func() { 39 | It("test metrics", func() { 40 | ts := httptest.NewServer(http.HandlerFunc(http.NotFound)) 41 | defer ts.Close() 42 | 43 | c, err := NewPrometheusClient(ts.URL) 44 | Expect(err).ToNot(HaveOccurred()) 45 | _, err = c.GetMetrics(context.TODO()) 46 | Expect(err).To(HaveOccurred()) 47 | 48 | mfs, err := getMetricsFamily() 49 | Expect(err).NotTo(HaveOccurred()) 50 | mf, ok := mfs["pvcautoresizer_metrics_client_fail_total"] 51 | Expect(ok).To(BeTrue()) 52 | 53 | var value int 54 | for _, m := range mf.Metric { 55 | if m.Counter == nil { 56 | continue 57 | } 58 | if m.Counter.Value == nil { 59 | continue 60 | } 61 | value = int(*m.Counter.Value) 62 | } 63 | Expect(value).NotTo(Equal(0)) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /internal/runners/prometheus_client.go: -------------------------------------------------------------------------------- 1 | package runners 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/prometheus/client_golang/api" 9 | prometheusv1 "github.com/prometheus/client_golang/api/prometheus/v1" 10 | "github.com/prometheus/common/model" 11 | "github.com/topolvm/pvc-autoresizer/internal/metrics" 12 | "k8s.io/apimachinery/pkg/types" 13 | ) 14 | 15 | // NewPrometheusClient returns a new prometheusClient 16 | func NewPrometheusClient(url string) (MetricsClient, error) { 17 | client, err := api.NewClient(api.Config{ 18 | Address: url, 19 | }) 20 | if err != nil { 21 | return nil, err 22 | } 23 | v1api := prometheusv1.NewAPI(client) 24 | 25 | return &prometheusClient{ 26 | prometheusAPI: v1api, 27 | }, nil 28 | } 29 | 30 | type prometheusClient struct { 31 | prometheusAPI prometheusv1.API 32 | } 33 | 34 | // GetMetrics implements MetricsClient.GetMetrics 35 | func (c *prometheusClient) GetMetrics(ctx context.Context) (map[types.NamespacedName]*VolumeStats, error) { 36 | volumeStatsMap := make(map[types.NamespacedName]*VolumeStats) 37 | 38 | availableBytes, err := c.getMetricValues(ctx, volumeAvailableQuery) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | capacityBytes, err := c.getMetricValues(ctx, volumeCapacityQuery) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | availableInodeSize, err := c.getMetricValues(ctx, inodesAvailableQuery) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | capacityInodeSize, err := c.getMetricValues(ctx, inodesCapacityQuery) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | for key, val := range availableBytes { 59 | vs := &VolumeStats{AvailableBytes: val} 60 | if cb, ok := capacityBytes[key]; ok { 61 | vs.CapacityBytes = cb 62 | } else { 63 | continue 64 | } 65 | if ais, ok := availableInodeSize[key]; ok { 66 | vs.AvailableInodeSize = ais 67 | } else { 68 | continue 69 | } 70 | if cis, ok := capacityInodeSize[key]; ok { 71 | vs.CapacityInodeSize = cis 72 | } else { 73 | continue 74 | } 75 | volumeStatsMap[key] = vs 76 | } 77 | 78 | return volumeStatsMap, nil 79 | } 80 | 81 | func (c *prometheusClient) getMetricValues(ctx context.Context, query string) (map[types.NamespacedName]int64, error) { 82 | res, _, err := c.prometheusAPI.Query(ctx, query, time.Now()) 83 | if err != nil { 84 | metrics.MetricsClientFailTotal.Increment() 85 | return nil, err 86 | } 87 | 88 | if res.Type() != model.ValVector { 89 | return nil, fmt.Errorf("unknown response type: %s", res.Type().String()) 90 | } 91 | resultMap := make(map[types.NamespacedName]int64) 92 | vec := res.(model.Vector) 93 | for _, val := range vec { 94 | nn := types.NamespacedName{ 95 | Namespace: string(val.Metric["namespace"]), 96 | Name: string(val.Metric["persistentvolumeclaim"]), 97 | } 98 | resultMap[nn] = int64(val.Value) 99 | } 100 | return resultMap, nil 101 | } 102 | -------------------------------------------------------------------------------- /internal/runners/pvc_autoresizer.go: -------------------------------------------------------------------------------- 1 | package runners 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/go-logr/logr" 12 | pvcautoresizer "github.com/topolvm/pvc-autoresizer" 13 | "github.com/topolvm/pvc-autoresizer/internal/metrics" 14 | corev1 "k8s.io/api/core/v1" 15 | storagev1 "k8s.io/api/storage/v1" 16 | "k8s.io/apimachinery/pkg/api/resource" 17 | "k8s.io/apimachinery/pkg/types" 18 | "k8s.io/client-go/tools/record" 19 | ctrl "sigs.k8s.io/controller-runtime" 20 | "sigs.k8s.io/controller-runtime/pkg/client" 21 | "sigs.k8s.io/controller-runtime/pkg/manager" 22 | ) 23 | 24 | //+kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;update;patch 25 | //+kubebuilder:rbac:groups=storage.k8s.io,resources=storageclasses,verbs=get;list;watch 26 | //+kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create 27 | 28 | const resizeEnableIndexKey = ".metadata.annotations[resize.topolvm.io/enabled]" 29 | const storageClassNameIndexKey = ".spec.storageClassName" 30 | const logLevelWarn = 3 31 | 32 | // NewPVCAutoresizer returns a new pvcAutoresizer struct 33 | func NewPVCAutoresizer(mc MetricsClient, c client.Client, log logr.Logger, interval time.Duration, 34 | recorder record.EventRecorder) manager.Runnable { 35 | 36 | return &pvcAutoresizer{ 37 | metricsClient: mc, 38 | client: c, 39 | log: log, 40 | interval: interval, 41 | recorder: recorder, 42 | } 43 | } 44 | 45 | type pvcAutoresizer struct { 46 | client client.Client 47 | metricsClient MetricsClient 48 | interval time.Duration 49 | log logr.Logger 50 | recorder record.EventRecorder 51 | } 52 | 53 | // Start implements manager.Runnable 54 | func (w *pvcAutoresizer) Start(ctx context.Context) error { 55 | ticker := time.NewTicker(w.interval) 56 | 57 | defer ticker.Stop() 58 | for { 59 | select { 60 | case <-ctx.Done(): 61 | return nil 62 | case <-ticker.C: 63 | startTime := time.Now() 64 | w.reconcile(ctx) 65 | metrics.ResizerLoopSecondsTotal.Add(time.Since(startTime).Seconds()) 66 | } 67 | } 68 | } 69 | 70 | func isTargetPVC(pvc *corev1.PersistentVolumeClaim) (bool, error) { 71 | quantity, err := PvcStorageLimit(pvc) 72 | if err != nil { 73 | return false, fmt.Errorf("invalid storage limit: %w", err) 74 | } 75 | if quantity.IsZero() { 76 | return false, nil 77 | } 78 | if pvc.Spec.VolumeMode != nil && *pvc.Spec.VolumeMode != corev1.PersistentVolumeFilesystem { 79 | return false, nil 80 | } 81 | if pvc.Status.Phase != corev1.ClaimBound { 82 | return false, nil 83 | } 84 | return true, nil 85 | } 86 | 87 | func (w *pvcAutoresizer) getStorageClassList(ctx context.Context) (*storagev1.StorageClassList, error) { 88 | var scs storagev1.StorageClassList 89 | err := w.client.List(ctx, &scs, client.MatchingFields(map[string]string{resizeEnableIndexKey: "true"})) 90 | if err != nil { 91 | metrics.KubernetesClientFailTotal.Increment() 92 | return nil, err 93 | } 94 | return &scs, nil 95 | } 96 | 97 | func (w *pvcAutoresizer) reconcile(ctx context.Context) { 98 | scs, err := w.getStorageClassList(ctx) 99 | if err != nil { 100 | w.log.Error(err, "getStorageClassList failed") 101 | return 102 | } 103 | 104 | vsMap, err := w.metricsClient.GetMetrics(ctx) 105 | if err != nil { 106 | w.log.Error(err, "metricsClient.GetMetrics failed") 107 | return 108 | } 109 | 110 | for _, sc := range scs.Items { 111 | var pvcs corev1.PersistentVolumeClaimList 112 | err = w.client.List(ctx, &pvcs, client.MatchingFields(map[string]string{storageClassNameIndexKey: sc.Name})) 113 | if err != nil { 114 | metrics.KubernetesClientFailTotal.Increment() 115 | w.log.Error(err, "list pvc failed") 116 | return 117 | } 118 | for _, pvc := range pvcs.Items { 119 | log := w.log.WithValues("namespace", pvc.Namespace, "name", pvc.Name) 120 | isTarget, err := isTargetPVC(&pvc) 121 | if err != nil { 122 | metrics.ResizerFailedResizeTotal.Increment(pvc.Name, pvc.Namespace) 123 | log.Error(err, "failed to check target PVC") 124 | continue 125 | } else if !isTarget { 126 | continue 127 | } 128 | 129 | // To output the metric even if some events do not occur, we call SpecifyLabels() here. 130 | metrics.ResizerSuccessResizeTotal.SpecifyLabels(pvc.Name, pvc.Namespace) 131 | metrics.ResizerFailedResizeTotal.SpecifyLabels(pvc.Name, pvc.Namespace) 132 | metrics.ResizerLimitReachedTotal.SpecifyLabels(pvc.Name, pvc.Namespace) 133 | 134 | namespacedName := types.NamespacedName{ 135 | Namespace: pvc.Namespace, 136 | Name: pvc.Name, 137 | } 138 | if _, ok := vsMap[namespacedName]; !ok { 139 | // Do not increment ResizerFailedResizeTotal here. The controller cannot get volume 140 | // stats for "offline" volumes (i.e. volumes not mounted by any pod) since kubelet 141 | // exports volume stats of a persistent volume claim only if it is online. Besides, 142 | // NodeExpandVolume RPC assumes that the volume to be published or staged on a node 143 | // (and hence online), the resize request of controller for offline PVC will not be 144 | // processed for the time being. So, we do not regard it as a resize failure that 145 | // the controller failed to retrieve volume stats for the PVC. This may result in a 146 | // failure to increment the counter in the case which the PVC is online but fails 147 | // to retrieve its metrics, but accept this as a limitation for now. 148 | log.Info("failed to get volume stats") 149 | continue 150 | } 151 | 152 | err = w.resize(ctx, &pvc, vsMap[namespacedName]) 153 | if err != nil { 154 | metrics.ResizerFailedResizeTotal.Increment(pvc.Name, pvc.Namespace) 155 | log.Error(err, "failed to resize PVC") 156 | } 157 | } 158 | } 159 | } 160 | 161 | func (w *pvcAutoresizer) resize(ctx context.Context, pvc *corev1.PersistentVolumeClaim, vs *VolumeStats) error { 162 | log := w.log.WithName("resize").WithValues("namespace", pvc.Namespace, "name", pvc.Name) 163 | 164 | threshold, err := convertSizeInBytes(pvc.Annotations[pvcautoresizer.ResizeThresholdAnnotation], vs.CapacityBytes, pvcautoresizer.DefaultThreshold) 165 | if err != nil { 166 | log.V(logLevelWarn).Info("failed to convert threshold annotation", "error", err.Error()) 167 | // lint:ignore nilerr ignores this because invalid annotations should be allowed. 168 | return nil 169 | } 170 | 171 | annotation := pvc.Annotations[pvcautoresizer.ResizeInodesThresholdAnnotation] 172 | inodesThreshold, err := convertSize(annotation, vs.CapacityInodeSize, pvcautoresizer.DefaultInodesThreshold) 173 | if err != nil { 174 | log.V(logLevelWarn).Info("failed to convert threshold annotation", "error", err.Error()) 175 | // lint:ignore nilerr ignores this because invalid annotations should be allowed. 176 | return nil 177 | } 178 | 179 | cap, exists := pvc.Status.Capacity[corev1.ResourceStorage] 180 | if !exists { 181 | log.Info("skip resizing because pvc capacity is not set yet") 182 | return nil 183 | } 184 | if cap.Value() == 0 { 185 | log.Info("skip resizing because pvc capacity size is zero") 186 | return nil 187 | } 188 | 189 | increase, err := convertSizeInBytes(pvc.Annotations[pvcautoresizer.ResizeIncreaseAnnotation], cap.Value(), pvcautoresizer.DefaultIncrease) 190 | if err != nil { 191 | log.V(logLevelWarn).Info("failed to convert increase annotation", "error", err.Error()) 192 | return nil 193 | } 194 | 195 | preCap, exist := pvc.Annotations[pvcautoresizer.PreviousCapacityBytesAnnotation] 196 | if exist { 197 | preCapInt64, err := strconv.ParseInt(preCap, 10, 64) 198 | if err != nil { 199 | log.V(logLevelWarn).Info("failed to parse pre_cap_bytes annotation", "error", err.Error()) 200 | // lint:ignore nilerr ignores this because invalid annotations should be allowed. 201 | return nil 202 | } 203 | if preCapInt64 == vs.CapacityBytes { 204 | log.Info("waiting for resizing...", "capacity", vs.CapacityBytes) 205 | return nil 206 | } 207 | } 208 | limitRes, err := PvcStorageLimit(pvc) 209 | if err != nil { 210 | log.Error(err, "fetching storage limit failed") 211 | return err 212 | } 213 | if cap.Cmp(limitRes) >= 0 { 214 | log.Info("volume storage limit reached") 215 | metrics.ResizerLimitReachedTotal.Increment(pvc.Name, pvc.Namespace) 216 | return nil 217 | } 218 | 219 | if threshold > vs.AvailableBytes || inodesThreshold > vs.AvailableInodeSize { 220 | if pvc.Annotations == nil { 221 | pvc.Annotations = make(map[string]string) 222 | } 223 | newReqBytes := int64(math.Ceil(float64(cap.Value()+increase)/(1<<30))) << 30 224 | newReq := resource.NewQuantity(newReqBytes, resource.BinarySI) 225 | if newReq.Cmp(limitRes) > 0 { 226 | newReq = &limitRes 227 | } 228 | 229 | pvc.Spec.Resources.Requests[corev1.ResourceStorage] = *newReq 230 | pvc.Annotations[pvcautoresizer.PreviousCapacityBytesAnnotation] = strconv.FormatInt(vs.CapacityBytes, 10) 231 | err = w.client.Update(ctx, pvc) 232 | if err != nil { 233 | metrics.KubernetesClientFailTotal.Increment() 234 | return err 235 | } 236 | log.Info("resize started", 237 | "from", cap.Value(), 238 | "to", newReq.Value(), 239 | "threshold", threshold, 240 | "available", vs.AvailableBytes, 241 | "inodesThreshold", inodesThreshold, 242 | "inodesAvailable", vs.AvailableInodeSize, 243 | ) 244 | w.recorder.Eventf(pvc, corev1.EventTypeNormal, "Resized", "PVC volume is resized to %s", newReq.String()) 245 | metrics.ResizerSuccessResizeTotal.Increment(pvc.Name, pvc.Namespace) 246 | } 247 | 248 | return nil 249 | } 250 | 251 | func indexByResizeEnableAnnotation(obj client.Object) []string { 252 | sc := obj.(*storagev1.StorageClass) 253 | if val, ok := sc.Annotations[pvcautoresizer.AutoResizeEnabledKey]; ok { 254 | return []string{val} 255 | } 256 | 257 | return []string{} 258 | } 259 | 260 | func indexByStorageClassName(obj client.Object) []string { 261 | pvc := obj.(*corev1.PersistentVolumeClaim) 262 | scName := pvc.Spec.StorageClassName 263 | if scName == nil { 264 | return []string{} 265 | } 266 | return []string{*scName} 267 | } 268 | 269 | // SetupIndexer setup indices for PVC auto resizer 270 | func SetupIndexer(mgr ctrl.Manager, skipAnnotationCheck bool) error { 271 | idxFunc := indexByResizeEnableAnnotation 272 | if skipAnnotationCheck { 273 | idxFunc = func(_ client.Object) []string { return []string{"true"} } 274 | } 275 | err := mgr.GetFieldIndexer().IndexField(context.Background(), &storagev1.StorageClass{}, resizeEnableIndexKey, idxFunc) 276 | if err != nil { 277 | return err 278 | } 279 | 280 | err = mgr.GetFieldIndexer().IndexField(context.Background(), &corev1.PersistentVolumeClaim{}, storageClassNameIndexKey, 281 | indexByStorageClassName) 282 | if err != nil { 283 | return err 284 | } 285 | 286 | return nil 287 | } 288 | 289 | func convertSizeInBytes(valStr string, capacity int64, defaultVal string) (int64, error) { 290 | if len(valStr) == 0 { 291 | valStr = defaultVal 292 | } 293 | if strings.HasSuffix(valStr, "%") { 294 | return calcSize(valStr, capacity) 295 | } 296 | 297 | quantity, err := resource.ParseQuantity(valStr) 298 | if err != nil { 299 | return 0, err 300 | } 301 | val := quantity.Value() 302 | if val <= 0 { 303 | return 0, fmt.Errorf("annotation value should be positive: %s", valStr) 304 | } 305 | return val, nil 306 | } 307 | 308 | func convertSize(valStr string, capacity int64, defaultVal string) (int64, error) { 309 | if len(valStr) == 0 { 310 | valStr = defaultVal 311 | } 312 | if strings.HasSuffix(valStr, "%") { 313 | return calcSize(valStr, capacity) 314 | } 315 | return 0, fmt.Errorf("annotation value should be in percent notation: %s", valStr) 316 | } 317 | 318 | func calcSize(valStr string, capacity int64) (int64, error) { 319 | rate, err := strconv.ParseFloat(strings.TrimRight(valStr, "%"), 64) 320 | if err != nil { 321 | return 0, err 322 | } 323 | if rate < 0 || rate > 100 { 324 | return 0, fmt.Errorf("annotation value should between 0 and 100: %s", valStr) 325 | } 326 | 327 | res := int64(float64(capacity) * rate / 100.0) 328 | return res, nil 329 | } 330 | 331 | func PvcStorageLimit(pvc *corev1.PersistentVolumeClaim) (resource.Quantity, error) { 332 | // storage limit on the annotation has precedence 333 | if annotation, ok := pvc.Annotations[pvcautoresizer.StorageLimitAnnotation]; ok && annotation != "" { 334 | return resource.ParseQuantity(annotation) 335 | } 336 | 337 | return *resource.NewQuantity(0, resource.BinarySI), nil 338 | } 339 | -------------------------------------------------------------------------------- /internal/runners/pvc_autoresizer_test.go: -------------------------------------------------------------------------------- 1 | package runners 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "time" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | pvcautoresizer "github.com/topolvm/pvc-autoresizer" 12 | corev1 "k8s.io/api/core/v1" 13 | "k8s.io/apimachinery/pkg/api/resource" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/types" 16 | ) 17 | 18 | var _ = Describe("test resizer", func() { 19 | Context("test convertSizeInBytes", func() { 20 | type input struct { 21 | valStr string 22 | capacity int64 23 | defaultVal string 24 | } 25 | type testCase struct { 26 | input input 27 | expect int64 28 | } 29 | correctCases := []testCase{ 30 | { 31 | input: input{ 32 | valStr: "", 33 | capacity: 100, 34 | defaultVal: "10%", 35 | }, 36 | expect: 10, 37 | }, 38 | { 39 | input: input{ 40 | valStr: "20%", 41 | capacity: 100, 42 | defaultVal: "10%", 43 | }, 44 | expect: 20, 45 | }, 46 | { 47 | input: input{ 48 | valStr: "30Gi", 49 | capacity: 40 << 30, 50 | defaultVal: "10%", 51 | }, 52 | expect: 30 << 30, 53 | }, 54 | { 55 | input: input{ 56 | valStr: "100%", 57 | capacity: 100, 58 | defaultVal: "10%", 59 | }, 60 | expect: 100, 61 | }, 62 | } 63 | errorCases := []input{ 64 | { 65 | valStr: "-10%", 66 | capacity: 100, 67 | defaultVal: "10%", 68 | }, 69 | { 70 | valStr: "101%", 71 | capacity: 100, 72 | defaultVal: "10%", 73 | }, 74 | { 75 | valStr: "-10Gi", 76 | capacity: 100, 77 | defaultVal: "10%", 78 | }, 79 | { 80 | valStr: "hoge", 81 | capacity: 100, 82 | defaultVal: "10%", 83 | }, 84 | } 85 | It("should be ok", func() { 86 | for _, val := range correctCases { 87 | res, err := convertSizeInBytes(val.input.valStr, val.input.capacity, val.input.defaultVal) 88 | Expect(err).ToNot(HaveOccurred()) 89 | Expect(res).To(Equal(val.expect)) 90 | } 91 | }) 92 | It("should be error", func() { 93 | for _, val := range errorCases { 94 | _, err := convertSizeInBytes(val.valStr, val.capacity, val.defaultVal) 95 | Expect(err).To(HaveOccurred(), "%+v", val) 96 | } 97 | }) 98 | }) 99 | 100 | Context("test convertSize", func() { 101 | type input struct { 102 | valStr string 103 | capacity int64 104 | defaultVal string 105 | } 106 | type testCase struct { 107 | input input 108 | expect int64 109 | } 110 | correctCases := []testCase{ 111 | { 112 | input: input{ 113 | valStr: "", 114 | capacity: 100, 115 | defaultVal: "10%", 116 | }, 117 | expect: 10, 118 | }, 119 | { 120 | input: input{ 121 | valStr: "20%", 122 | capacity: 100, 123 | defaultVal: "10%", 124 | }, 125 | expect: 20, 126 | }, 127 | } 128 | errorCases := []input{ 129 | { 130 | valStr: "10", 131 | capacity: 100, 132 | defaultVal: "10%", 133 | }, 134 | { 135 | valStr: "-10%", 136 | capacity: 100, 137 | defaultVal: "10%", 138 | }, 139 | { 140 | valStr: "101%", 141 | capacity: 100, 142 | defaultVal: "10%", 143 | }, 144 | { 145 | valStr: "hoge", 146 | capacity: 100, 147 | defaultVal: "10%", 148 | }, 149 | } 150 | It("should be ok", func() { 151 | for _, val := range correctCases { 152 | res, err := convertSize(val.input.valStr, val.input.capacity, val.input.defaultVal) 153 | Expect(err).ToNot(HaveOccurred()) 154 | Expect(res).To(Equal(val.expect)) 155 | } 156 | }) 157 | It("should be error", func() { 158 | for _, val := range errorCases { 159 | _, err := convertSize(val.valStr, val.capacity, val.defaultVal) 160 | Expect(err).To(HaveOccurred(), "%+v", val) 161 | } 162 | }) 163 | }) 164 | 165 | Context("resize", func() { 166 | Context("parameter tests", func() { 167 | ctx := context.Background() 168 | pvcNS := "default" 169 | increase := "1Gi" 170 | limit := int64(100 << 30) 171 | volumeMode := corev1.PersistentVolumeFilesystem 172 | 173 | testCases := []struct { 174 | description string 175 | pvcSizeGi int64 176 | pvcCapSizeGi int64 177 | expectSizeGi int64 178 | threshold string 179 | availableByte int64 180 | capacityInodeSize int64 181 | availableInodeSize int64 182 | inodesThreshold string 183 | }{ 184 | { 185 | description: "Should resize(absolute value)", 186 | pvcSizeGi: 10, 187 | pvcCapSizeGi: 10, 188 | expectSizeGi: 11, 189 | threshold: "5Gi", 190 | availableByte: 5<<30 - 1, 191 | availableInodeSize: 100, 192 | capacityInodeSize: 100, 193 | }, 194 | { 195 | description: "Should not resize(absolute value)", 196 | pvcSizeGi: 10, 197 | pvcCapSizeGi: 10, 198 | expectSizeGi: 10, 199 | threshold: "5Gi", 200 | availableByte: 5 << 30, 201 | availableInodeSize: 100, 202 | capacityInodeSize: 100, 203 | }, 204 | { 205 | description: "Should resize(%)", 206 | pvcSizeGi: 10, 207 | pvcCapSizeGi: 10, 208 | expectSizeGi: 11, 209 | threshold: "50%", 210 | availableByte: 5<<30 - 1, 211 | availableInodeSize: 100, 212 | capacityInodeSize: 100, 213 | }, 214 | { 215 | description: "Should not resize(%)", 216 | pvcSizeGi: 10, 217 | pvcCapSizeGi: 10, 218 | expectSizeGi: 10, 219 | threshold: "50%", 220 | availableByte: 5 << 30, 221 | availableInodeSize: 100, 222 | capacityInodeSize: 100, 223 | }, 224 | { 225 | description: "Should resize(inode)", 226 | pvcSizeGi: 10, 227 | pvcCapSizeGi: 10, 228 | expectSizeGi: 11, 229 | threshold: "50%", 230 | availableByte: 5 << 30, 231 | availableInodeSize: 9, 232 | capacityInodeSize: 100, 233 | }, 234 | { 235 | description: "Should resize(inode with annotation)", 236 | pvcSizeGi: 10, 237 | pvcCapSizeGi: 10, 238 | expectSizeGi: 11, 239 | threshold: "50%", 240 | availableByte: 5 << 30, 241 | availableInodeSize: 49, 242 | capacityInodeSize: 100, 243 | inodesThreshold: "50%", 244 | }, 245 | { 246 | description: "Should not resize(inode)", 247 | pvcSizeGi: 10, 248 | pvcCapSizeGi: 10, 249 | expectSizeGi: 10, 250 | threshold: "50%", 251 | availableByte: 5 << 30, 252 | availableInodeSize: 9, 253 | capacityInodeSize: 100, 254 | inodesThreshold: "0%", 255 | }, 256 | { 257 | description: "Should resize(capacity size check)", 258 | pvcSizeGi: 1, 259 | pvcCapSizeGi: 10, 260 | expectSizeGi: 11, 261 | threshold: "5Gi", 262 | availableByte: 5<<30 - 1, 263 | availableInodeSize: 100, 264 | capacityInodeSize: 100, 265 | }, 266 | { 267 | description: "Should not resize(inode - 0 capacityInodeSize)", 268 | pvcSizeGi: 10, 269 | pvcCapSizeGi: 10, 270 | expectSizeGi: 10, 271 | threshold: "50%", 272 | availableByte: 5 << 30, 273 | availableInodeSize: 0, 274 | capacityInodeSize: 0, 275 | inodesThreshold: "20%", 276 | }, 277 | { 278 | description: "Should not resize(no capacity value set)", 279 | pvcSizeGi: 10, 280 | pvcCapSizeGi: -1, 281 | expectSizeGi: 10, 282 | threshold: "5Gi", 283 | availableByte: 5<<30 - 1, 284 | availableInodeSize: 100, 285 | capacityInodeSize: 100, 286 | }, 287 | } 288 | 289 | for i, tc := range testCases { 290 | pvcName := fmt.Sprintf("test-pvc-%d", i) 291 | pvcSizeGi := tc.pvcSizeGi 292 | pvcCapSizeGi := tc.pvcCapSizeGi 293 | expectSizeGi := tc.expectSizeGi 294 | threshold := tc.threshold 295 | availableByte := tc.availableByte 296 | availableInodeSize := tc.availableInodeSize 297 | capacityInodeSize := tc.capacityInodeSize 298 | inodesThreshold := tc.inodesThreshold 299 | 300 | description := fmt.Sprintf( 301 | "%s: pvcSizeGi=%d expectSizeGi=%d threshold=%q availableByte=%d availableInodeSize=%d "+ 302 | "capacityInodeSize=%d inodesThreshold=%q", 303 | tc.description, 304 | tc.pvcSizeGi, 305 | tc.expectSizeGi, 306 | tc.threshold, 307 | tc.availableByte, 308 | availableInodeSize, 309 | capacityInodeSize, 310 | inodesThreshold) 311 | 312 | It(description, func() { 313 | createPVC(ctx, pvcNS, pvcName, scName, threshold, inodesThreshold, increase, pvcSizeGi<<30, limit, 314 | pvcCapSizeGi<<30, volumeMode) 315 | setMetrics(pvcNS, pvcName, availableByte, pvcSizeGi<<30, availableInodeSize, capacityInodeSize) 316 | testFunc := func() error { 317 | var pvc corev1.PersistentVolumeClaim 318 | err := k8sClient.Get(ctx, types.NamespacedName{Namespace: pvcNS, Name: pvcName}, &pvc) 319 | if err != nil { 320 | return err 321 | } 322 | req := pvc.Spec.Resources.Requests.Storage().Value() 323 | 324 | ALLOWANCE := int64(1 << 10) 325 | if !(expectSizeGi<<30-ALLOWANCE < req && req <= expectSizeGi<<30+ALLOWANCE) { 326 | return fmt.Errorf("request size(Gi) should be %d, but %d", expectSizeGi, req>>30) 327 | } 328 | return nil 329 | } 330 | 331 | if pvcSizeGi == expectSizeGi { 332 | Consistently(testFunc, 3*time.Second).ShouldNot(HaveOccurred()) 333 | } else { 334 | Eventually(testFunc, 3*time.Second).ShouldNot(HaveOccurred()) 335 | } 336 | }) 337 | } 338 | }) 339 | 340 | Context("metrics tests", func() { 341 | It("should output metrics", func() { 342 | ctx := context.Background() 343 | pvcNS := "default" 344 | pvcName := "test-resize-metrics" 345 | createPVC(ctx, pvcNS, pvcName, scName, "50%", "", "20Gi", 10<<30, 100<<30, 10<<30, 346 | corev1.PersistentVolumeFilesystem) 347 | By("running resize", func() { 348 | setMetrics(pvcNS, pvcName, 3<<30, 7<<30, 2050246, 2050246) 349 | Eventually(func() error { 350 | var pvc corev1.PersistentVolumeClaim 351 | err := k8sClient.Get(ctx, types.NamespacedName{Namespace: pvcNS, Name: pvcName}, &pvc) 352 | if err != nil { 353 | return err 354 | } 355 | req := pvc.Spec.Resources.Requests.Storage().Value() 356 | if req != 30<<30 { 357 | return fmt.Errorf("request size should be %d, but %d", 30<<30, req) 358 | } 359 | return nil 360 | }, 3*time.Second).ShouldNot(HaveOccurred()) 361 | }) 362 | 363 | By("checking metrics", func() { 364 | mfs, err := getMetricsFamily() 365 | Expect(err).NotTo(HaveOccurred()) 366 | mf, ok := mfs["pvcautoresizer_loop_seconds_total"] 367 | Expect(ok).To(BeTrue()) 368 | 369 | var val float64 370 | for _, m := range mf.Metric { 371 | if m.Counter == nil { 372 | continue 373 | } 374 | if m.Counter.Value == nil { 375 | continue 376 | } 377 | val = *m.Counter.Value 378 | } 379 | Expect(val).NotTo(Equal(float64(0))) 380 | 381 | mf, ok = mfs["pvcautoresizer_success_resize_total"] 382 | Expect(ok).To(BeTrue()) 383 | var val2 int 384 | for _, m := range mf.Metric { 385 | if m.Counter == nil { 386 | continue 387 | } 388 | if m.Counter.Value == nil { 389 | continue 390 | } 391 | val2 = int(*m.Counter.Value) 392 | } 393 | Expect(val2).NotTo(Equal(0)) 394 | 395 | // This metrics output from the pvcAutoresizer with FakeClientWrapper 396 | mf, ok = mfs["pvcautoresizer_failed_resize_total"] 397 | Expect(ok).To(BeTrue()) 398 | var val3 int 399 | for _, m := range mf.Metric { 400 | if m.Counter == nil { 401 | continue 402 | } 403 | if m.Counter.Value == nil { 404 | continue 405 | } 406 | val3 = int(*m.Counter.Value) 407 | } 408 | Expect(val3).NotTo(Equal(0)) 409 | 410 | // This metrics output from the pvcAutoresizer with FakeClientWrapper 411 | mf, ok = mfs["pvcautoresizer_kubernetes_client_fail_total"] 412 | Expect(ok).To(BeTrue()) 413 | var val4 int 414 | for _, m := range mf.Metric { 415 | if m.Counter == nil { 416 | continue 417 | } 418 | if m.Counter.Value == nil { 419 | continue 420 | } 421 | val4 = int(*m.Counter.Value) 422 | } 423 | Expect(val4).NotTo(Equal(0)) 424 | }) 425 | }) 426 | }) 427 | }) 428 | }) 429 | 430 | func createPVC(ctx context.Context, ns, name, scName, threshold, inodesThreshold, increase string, 431 | request, limit, capacity int64, mode corev1.PersistentVolumeMode) { 432 | pvc := corev1.PersistentVolumeClaim{ 433 | ObjectMeta: metav1.ObjectMeta{ 434 | Name: name, 435 | Namespace: ns, 436 | Annotations: map[string]string{}, 437 | }, 438 | Spec: corev1.PersistentVolumeClaimSpec{ 439 | Resources: corev1.VolumeResourceRequirements{ 440 | Requests: corev1.ResourceList{ 441 | corev1.ResourceStorage: *resource.NewQuantity(request, resource.BinarySI), 442 | }, 443 | }, 444 | AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, 445 | StorageClassName: &scName, 446 | VolumeMode: &mode, 447 | }, 448 | } 449 | 450 | if len(threshold) != 0 { 451 | pvc.Annotations[pvcautoresizer.ResizeThresholdAnnotation] = threshold 452 | } 453 | if len(inodesThreshold) != 0 { 454 | pvc.Annotations[pvcautoresizer.ResizeInodesThresholdAnnotation] = inodesThreshold 455 | } 456 | 457 | if len(increase) != 0 { 458 | pvc.Annotations[pvcautoresizer.ResizeIncreaseAnnotation] = increase 459 | } 460 | 461 | if limit != 0 { 462 | pvc.Annotations[pvcautoresizer.StorageLimitAnnotation] = strconv.FormatInt(limit, 10) 463 | } 464 | 465 | err := k8sClient.Create(ctx, &pvc) 466 | Expect(err).NotTo(HaveOccurred()) 467 | 468 | pvc.Status.Phase = corev1.ClaimBound 469 | if capacity >= 0 { 470 | pvc.Status.Capacity = map[corev1.ResourceName]resource.Quantity{ 471 | corev1.ResourceStorage: *resource.NewQuantity(capacity, resource.BinarySI), 472 | } 473 | } 474 | err = k8sClient.Status().Update(ctx, &pvc) 475 | Expect(err).NotTo(HaveOccurred()) 476 | } 477 | 478 | func setMetrics(ns, name string, availableBytes, capacityBytes, availableInodeSize, capacityInodeSize int64) { 479 | promClient.setResponce(types.NamespacedName{ 480 | Namespace: ns, 481 | Name: name, 482 | }, &VolumeStats{ 483 | AvailableBytes: availableBytes, 484 | CapacityBytes: capacityBytes, 485 | AvailableInodeSize: availableInodeSize, 486 | CapacityInodeSize: capacityInodeSize, 487 | }) 488 | } 489 | -------------------------------------------------------------------------------- /internal/runners/suite_test.go: -------------------------------------------------------------------------------- 1 | package runners 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | "time" 10 | 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | dto "github.com/prometheus/client_model/go" 14 | "github.com/prometheus/common/expfmt" 15 | pvcautoresizer "github.com/topolvm/pvc-autoresizer" 16 | corev1 "k8s.io/api/core/v1" 17 | storagev1 "k8s.io/api/storage/v1" 18 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 | "k8s.io/apimachinery/pkg/runtime" 20 | "k8s.io/client-go/rest" 21 | ctrl "sigs.k8s.io/controller-runtime" 22 | "sigs.k8s.io/controller-runtime/pkg/client" 23 | "sigs.k8s.io/controller-runtime/pkg/envtest" 24 | logf "sigs.k8s.io/controller-runtime/pkg/log" 25 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 26 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 27 | //+kubebuilder:scaffold:imports 28 | ) 29 | 30 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 31 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 32 | 33 | var cfg *rest.Config 34 | var k8sClient client.Client 35 | var testEnv *envtest.Environment 36 | var cancelMgr func() 37 | var promClient = prometheusClientMock{} 38 | 39 | var scName string = "test-storageclass" 40 | var provName string = "test-provisioner" 41 | 42 | func TestRunners(t *testing.T) { 43 | RegisterFailHandler(Fail) 44 | 45 | RunSpecs(t, "Controller Suite") 46 | } 47 | 48 | var _ = BeforeSuite(func() { 49 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter))) 50 | 51 | By("bootstrapping test environment") 52 | testEnv = &envtest.Environment{ 53 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 54 | } 55 | 56 | var err error 57 | cfg, err = testEnv.Start() 58 | Expect(err).ToNot(HaveOccurred()) 59 | Expect(cfg).ToNot(BeNil()) 60 | 61 | scheme := runtime.NewScheme() 62 | err = corev1.AddToScheme(scheme) 63 | Expect(err).NotTo(HaveOccurred()) 64 | 65 | err = storagev1.AddToScheme(scheme) 66 | Expect(err).NotTo(HaveOccurred()) 67 | 68 | //+kubebuilder:scaffold:scheme 69 | 70 | mgr, err := ctrl.NewManager(cfg, ctrl.Options{ 71 | Scheme: scheme, 72 | Metrics: metricsserver.Options{ 73 | BindAddress: ":8080", 74 | }, 75 | }) 76 | Expect(err).ToNot(HaveOccurred()) 77 | 78 | noCheck := os.Getenv("NO_ANNOTATION_CHECK") == "true" 79 | err = SetupIndexer(mgr, noCheck) 80 | Expect(err).ToNot(HaveOccurred()) 81 | 82 | pvcAutoresizer := NewPVCAutoresizer(&promClient, mgr.GetClient(), 83 | logf.Log.WithName("pvc-autoresizer"), 84 | 1*time.Second, mgr.GetEventRecorderFor("pvc-autoresizer")) 85 | err = mgr.Add(pvcAutoresizer) 86 | Expect(err).ToNot(HaveOccurred()) 87 | 88 | // Add pvcAutoresizer with FakeClientWrapper for metrics tests 89 | pvcAutoresizer2 := NewPVCAutoresizer(&promClient, NewFakeClientWrapper(mgr.GetClient()), 90 | logf.Log.WithName("pvc-autoresizer2"), 91 | 1*time.Second, mgr.GetEventRecorderFor("pvc-autoresizer2")) 92 | err = mgr.Add(pvcAutoresizer2) 93 | Expect(err).ToNot(HaveOccurred()) 94 | 95 | ctx, cancel := context.WithCancel(context.Background()) 96 | cancelMgr = cancel 97 | go func() { 98 | err = mgr.Start(ctx) 99 | if err != nil { 100 | mgr.GetLogger().Error(err, "failed to start manager") 101 | } 102 | }() 103 | 104 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) 105 | Expect(err).ToNot(HaveOccurred()) 106 | Expect(k8sClient).ToNot(BeNil()) 107 | 108 | createStorageClass(ctx, scName, provName) 109 | }) 110 | 111 | var _ = AfterSuite(func() { 112 | By("tearing down the test environment") 113 | cancelMgr() 114 | time.Sleep(10 * time.Millisecond) 115 | err := testEnv.Stop() 116 | Expect(err).ToNot(HaveOccurred()) 117 | }) 118 | 119 | func createStorageClass(ctx context.Context, name, provisioner string) { 120 | t := true 121 | sc := storagev1.StorageClass{ 122 | ObjectMeta: metav1.ObjectMeta{ 123 | Name: name, 124 | Annotations: map[string]string{ 125 | pvcautoresizer.AutoResizeEnabledKey: "true", 126 | }, 127 | }, 128 | Provisioner: provisioner, 129 | AllowVolumeExpansion: &t, 130 | } 131 | err := k8sClient.Create(ctx, &sc) 132 | Expect(err).NotTo(HaveOccurred()) 133 | } 134 | 135 | func getMetricsFamily() (map[string]*dto.MetricFamily, error) { 136 | resp, err := http.Get("http://localhost:8080/metrics") 137 | if err != nil { 138 | return nil, err 139 | } 140 | defer func() { 141 | if err := resp.Body.Close(); err != nil { 142 | logf.Log.Error(err, "failed to close response body") 143 | } 144 | }() 145 | 146 | var parser expfmt.TextParser 147 | return parser.TextToMetricFamilies(resp.Body) 148 | } 149 | -------------------------------------------------------------------------------- /test/e2e/.gitignore: -------------------------------------------------------------------------------- 1 | *.img 2 | *.test 3 | /kube-prometheus 4 | -------------------------------------------------------------------------------- /test/e2e/Makefile: -------------------------------------------------------------------------------- 1 | include ../../versions.mk 2 | 3 | KIND_CLUSTER_NAME := autoresizer-e2e 4 | 5 | export KUBERNETES_VERSION 6 | 7 | SUDO := sudo 8 | BINDIR := $(shell pwd)/../../bin 9 | TMPDIR := /tmp/autoresizer 10 | KUBECTL := $(BINDIR)/kubectl 11 | HELM := $(BINDIR)/helm 12 | GINKGO := $(BINDIR)/ginkgo 13 | KIND := $(TMPDIR)/topolvm/bin/kind 14 | 15 | .PHONY: setup 16 | setup: 17 | $(MAKE) -C ../../ setup 18 | mkdir -p $(BINDIR) 19 | GOBIN=$(BINDIR) go install github.com/onsi/ginkgo/v2/ginkgo@v$(GINKGO_VERSION) 20 | 21 | .PHONY: init-cluster 22 | init-cluster: launch-kind autoresizer.img kube-prometheus 23 | # https://github.com/prometheus-operator/kube-prometheus/tree/v$(KUBE_PROMETHEUS_VERSION)#quickstart" 24 | # add options to avoid applying error (https://github.com/prometheus-community/helm-charts/issues/1500#issuecomment-969149744) 25 | $(KUBECTL) apply -f kube-prometheus/manifests/setup --force-conflicts=true --server-side 26 | until $(KUBECTL) get servicemonitors --all-namespaces ; do date; sleep 1; echo ""; done 27 | $(KUBECTL) apply -f kube-prometheus/manifests/ 28 | # prometheus-adapter is required to get "metrics.k8s.io/v1beta1" 29 | $(KUBECTL) wait deployment -n monitoring prometheus-adapter --for=condition=Available=True --timeout=120s 30 | sleep 10 31 | # setup autoresizer 32 | $(KIND) load image-archive --name=$(KIND_CLUSTER_NAME) autoresizer.img 33 | $(HELM) repo add jetstack https://charts.jetstack.io 34 | $(HELM) repo update 35 | $(HELM) dependency build ../../charts/pvc-autoresizer/ 36 | # storageclass for test 37 | $(KUBECTL) apply -f manifests/common/storageclass.yaml 38 | 39 | .PHONY: init-app-with-cert-manager 40 | init-app-with-cert-manager: init-cluster 41 | $(HELM) install --create-namespace --namespace=pvc-autoresizer pvc-autoresizer ../../charts/pvc-autoresizer/ -f manifests/values/values.yaml 42 | 43 | .PHONY: init-app-without-cert-manager 44 | init-app-without-cert-manager: init-cluster 45 | $(HELM) install --create-namespace --namespace=pvc-autoresizer pvc-autoresizer ../../charts/pvc-autoresizer/ -f manifests/values/values-without-cert-manager.yaml 46 | 47 | .PHONY: init-app-with-metrics-api 48 | init-app-with-metrics-api: init-cluster 49 | $(HELM) install --create-namespace --namespace=pvc-autoresizer pvc-autoresizer ../../charts/pvc-autoresizer/ -f manifests/values/values-with-metrics-api.yaml 50 | 51 | .PHONY: test 52 | test: 53 | E2ETEST=1 BINDIR=$(BINDIR) $(GINKGO) --fail-fast -v . 54 | 55 | .PHONY: launch-kind 56 | launch-kind: $(TMPDIR)/topolvm 57 | cp autoresizer-cluster.yaml $(TMPDIR)/topolvm/example/kind/topolvm-cluster.yaml 58 | make -C $(TMPDIR)/topolvm/example run \ 59 | KUBERNETES_VERSION=$(KUBERNETES_VERSION) \ 60 | KIND_CLUSTER_NAME=$(KIND_CLUSTER_NAME) \ 61 | TMPDIR=$(TMPDIR) 62 | $(KUBECTL) delete pod my-pod-thin 63 | $(KUBECTL) delete pvc topolvm-pvc-thin 64 | $(KUBECTL) delete sc topolvm-provisioner-thin 65 | sudo lvremove -f myvg1/thinpool 66 | 67 | .PHONY: shutdown-kind 68 | shutdown-kind: 69 | $(call call-topolvm-make-task,$@) 70 | 71 | .PHONY: stop-lvmd 72 | stop-lvmd: 73 | $(call call-topolvm-make-task,$@) 74 | 75 | define call-topolvm-make-task 76 | if [ -d $(TMPDIR)/topolvm/example ]; then make -C $(TMPDIR)/topolvm/example $(1) KIND_CLUSTER_NAME=$(KIND_CLUSTER_NAME) TMPDIR=$(TMPDIR); fi 77 | endef 78 | 79 | $(TMPDIR)/topolvm: 80 | git clone https://github.com/topolvm/topolvm.git $@ 81 | cd $@ && git checkout $(TOPOLVM_VERSION) 82 | make -C $(TMPDIR)/topolvm/example setup 83 | 84 | autoresizer.img: 85 | IMAGE_PREFIX="" $(MAKE) -C ../../ image 86 | docker save -o $@ pvc-autoresizer:devel 87 | 88 | kube-prometheus: 89 | mkdir $(shell pwd)/kube-prometheus && \ 90 | curl -sSfL https://github.com/prometheus-operator/kube-prometheus/archive/refs/tags/v$(KUBE_PROMETHEUS_VERSION).tar.gz \ 91 | | tar xfz - -C $(shell pwd)/kube-prometheus --strip-components=1 kube-prometheus-$(KUBE_PROMETHEUS_VERSION)/manifests 92 | rm $(shell pwd)/kube-prometheus/manifests/*networkPolicy.yaml 93 | 94 | .PHONY: clean 95 | clean: stop-lvmd 96 | for f in $$($(SUDO) find $(TMPDIR) -type f); do \ 97 | if $(SUDO) mountpoint -q $$f; then \ 98 | $(SUDO) umount $$f; \ 99 | fi; \ 100 | done 101 | $(SUDO) rm -rf $(TMPDIR)/controller $(TMPDIR)/worker 102 | rm -rf \ 103 | $(TMPDIR) \ 104 | autoresizer.img \ 105 | kube-prometheus 106 | -------------------------------------------------------------------------------- /test/e2e/README.md: -------------------------------------------------------------------------------- 1 | End-to-end tests of pvc-autoresizer using kind and Topolvm 2 | ===================================== 3 | 4 | This directory contains codes for end-to-end tests of pvc-autoresizer. 5 | The tests run using Topolvm and [kind (Kubernetes IN Docker)][kind] to make an environment with a `lvmd` running as systemd service. 6 | 7 | Setup environment 8 | ----------------- 9 | 10 | 1. Prepare Ubuntu machine. 11 | 2. [Install Docker CE](https://docs.docker.com/install/linux/docker-ce/ubuntu/#install-using-the-repository). 12 | 3. Add yourself to `docker` group. e.g. `sudo adduser $USER docker` 13 | 4. Run `make setup`. 14 | 5. Run `make init-app-with-cert-manager`. 15 | 16 | How to run tests 17 | ---------------- 18 | 19 | ### Run tests 20 | 21 | Run the tests with the following command. Repeat it until you get satisfied. 22 | When tests fail, use `kubectl` to inspect the Kubernetes cluster. 23 | 24 | ```console 25 | make test 26 | ``` 27 | 28 | ### Cleanup 29 | 30 | You can cleanup test environment as follows: 31 | 32 | ``` 33 | # cleanup all test environment 34 | make clean 35 | ``` 36 | 37 | [kind]: https://github.com/kubernetes-sigs/kind 38 | -------------------------------------------------------------------------------- /test/e2e/autoresizer-cluster.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kind.x-k8s.io/v1alpha4 2 | kind: Cluster 3 | # patch the generated kubeadm config with some extra settings 4 | kubeadmConfigPatches: 5 | - | 6 | apiVersion: "kubeadm.k8s.io/v1beta3" 7 | kind: ClusterConfiguration 8 | metadata: 9 | name: config 10 | kubernetesVersion: "v@KUBERNETES_VERSION@" 11 | networking: 12 | serviceSubnet: 10.0.0.0/16 13 | - | 14 | apiVersion: "kubeadm.k8s.io/v1beta3" 15 | kind: JoinConfiguration 16 | metadata: 17 | name: config 18 | nodeRegistration: 19 | kubeletExtraArgs: 20 | volume-stats-agg-period: 1s # speed up metric updates such as kubelet_volume_stats_used_bytes 21 | nodes: 22 | - role: control-plane 23 | extraMounts: 24 | - containerPath: /var/lib/kubelet 25 | hostPath: /tmp/autoresizer/controller 26 | propagation: Bidirectional 27 | - role: worker 28 | extraMounts: 29 | - containerPath: /run/topolvm 30 | hostPath: /tmp/autoresizer/lvmd 31 | - containerPath: /var/lib/kubelet 32 | hostPath: /tmp/autoresizer/worker 33 | propagation: Bidirectional 34 | - containerPath: /dev 35 | hostPath: /dev 36 | -------------------------------------------------------------------------------- /test/e2e/manifests/common/storageclass.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: StorageClass 3 | apiVersion: storage.k8s.io/v1 4 | metadata: 5 | name: topolvm-provisioner-annotated 6 | annotations: 7 | resize.topolvm.io/enabled: "true" 8 | provisioner: topolvm.io 9 | parameters: 10 | "csi.storage.k8s.io/fstype": "xfs" 11 | volumeBindingMode: WaitForFirstConsumer 12 | allowVolumeExpansion: true 13 | -------------------------------------------------------------------------------- /test/e2e/manifests/values/values-with-metrics-api.yaml: -------------------------------------------------------------------------------- 1 | image: 2 | repository: pvc-autoresizer 3 | tag: devel 4 | pullPolicy: Never 5 | 6 | controller: 7 | args: 8 | useK8sMetricsApi: true 9 | interval: 1s 10 | -------------------------------------------------------------------------------- /test/e2e/manifests/values/values-without-cert-manager.yaml: -------------------------------------------------------------------------------- 1 | image: 2 | repository: pvc-autoresizer 3 | tag: devel 4 | pullPolicy: Never 5 | 6 | controller: 7 | args: 8 | prometheusURL: http://prometheus-k8s.monitoring.svc:9090 9 | interval: 1s 10 | 11 | webhook: 12 | certificate: 13 | generate: true 14 | -------------------------------------------------------------------------------- /test/e2e/manifests/values/values.yaml: -------------------------------------------------------------------------------- 1 | image: 2 | repository: pvc-autoresizer 3 | tag: devel 4 | pullPolicy: Never 5 | 6 | controller: 7 | args: 8 | prometheusURL: http://prometheus-k8s.monitoring.svc:9090 9 | interval: 1s 10 | -------------------------------------------------------------------------------- /test/e2e/testdata/pod-pvc-template.yaml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolumeClaim 2 | apiVersion: v1 3 | metadata: 4 | name: {{ .pvcName }} 5 | namespace: {{ .namespace }} 6 | {{ if .useLabel }} 7 | labels: 8 | {{ range $key, $value := .labels }} 9 | {{ $key }}: {{ $value }} 10 | {{ end }} 11 | {{ end }} 12 | annotations: 13 | resize.topolvm.io/threshold: "{{ .thresholdAnnotation }}" 14 | resize.topolvm.io/increase: "{{ .increaseAnnotation }}" 15 | resize.topolvm.io/inodes-threshold: "{{ .inodesThresholdAnnotation }}" 16 | resize.topolvm.io/storage_limit: "{{ .storageLimitAnnotation }}" 17 | resize.topolvm.io/initial-resize-group-by: "{{ .initialResizeGroupByAnnotation }}" 18 | spec: 19 | accessModes: 20 | - ReadWriteOnce 21 | resources: 22 | requests: 23 | storage: {{ .resourceRequest }} 24 | storageClassName: {{ .storageClassName }} 25 | volumeMode: {{ .volumeMode }} 26 | --- 27 | apiVersion: v1 28 | kind: Pod 29 | metadata: 30 | name: {{ .pvcName }} 31 | namespace: {{ .namespace }} 32 | labels: 33 | app.kubernetes.io/name: ubuntu 34 | spec: 35 | containers: 36 | - name: ubuntu 37 | image: ubuntu:22.04 38 | command: 39 | - bash 40 | - -c 41 | - | 42 | sleep inf & 43 | trap "kill -SIGTERM $!" SIGTERM 44 | wait $! 45 | exit 46 | {{ if eq .volumeMode "Block" }} 47 | volumeDevices: 48 | - name: my-volume 49 | devicePath: /dev/e2etest 50 | {{ else }} 51 | volumeMounts: 52 | - mountPath: /test1 53 | name: my-volume 54 | {{ end }} 55 | volumes: 56 | - name: my-volume 57 | persistentVolumeClaim: 58 | claimName: {{ .pvcName }} 59 | -------------------------------------------------------------------------------- /versions.mk: -------------------------------------------------------------------------------- 1 | # https://github.com/helm/chart-testing/releases 2 | CHART_TESTING_VERSION := 3.12.0 3 | # https://github.com/kubernetes-sigs/controller-tools/releases 4 | CONTROLLER_TOOLS_VERSION := 0.17.2 5 | # https://github.com/golangci/golangci-lint/releases 6 | GOLANGCI_LINT_VERSION := v1.64.8 7 | # https://github.com/norwoodj/helm-docs/releases 8 | HELM_DOCS_VERSION := 1.14.2 9 | # https://github.com/helm/helm/releases 10 | HELM_VERSION := 3.17.2 11 | # https://github.com/prometheus-operator/kube-prometheus/releases 12 | KUBE_PROMETHEUS_VERSION := 0.14.0 13 | # It is set by CI using the environment variable, use conditional assignment. 14 | KUBERNETES_VERSION ?= 1.32.2 15 | TOPOLVM_VERSION := fa025f38c687278df6983a05bd5ebc27607dec4e 16 | 17 | # Tools versions which are defined in go.mod 18 | SELF_DIR := $(dir $(lastword $(MAKEFILE_LIST))) 19 | CONTROLLER_RUNTIME_VERSION := $(shell awk '/sigs\.k8s\.io\/controller-runtime/ {print substr($$2, 2)}' $(SELF_DIR)/go.mod) 20 | 21 | ENVTEST_BRANCH := release-$(shell echo $(CONTROLLER_RUNTIME_VERSION) | cut -d "." -f 1-2) 22 | ENVTEST_K8S_VERSION := $(shell echo $(KUBERNETES_VERSION) | cut -d "." -f 1-2) 23 | 24 | # Tools versions which are defined in go.mod 25 | SELF_DIR := $(dir $(lastword $(MAKEFILE_LIST))) 26 | GINKGO_VERSION := $(shell awk '/github.com\/onsi\/ginkgo\/v2/ {print substr($$2, 2)}' $(SELF_DIR)/go.mod) 27 | --------------------------------------------------------------------------------