├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── INSTALL.md ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── USAGE.md ├── api └── v1 │ ├── custompodautoscaler_types.go │ ├── groupversion_info.go │ └── zz_generated.deepcopy.go ├── controllers ├── custompodautoscaler_controller.go └── custompodautoscaler_controller_test.go ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── helm ├── .helmignore ├── Chart.yaml ├── templates │ ├── NOTES.txt │ ├── cluster │ │ ├── cluster_role.yaml │ │ ├── cluster_role_binding.yaml │ │ ├── operator.yaml │ │ └── service_account.yaml │ ├── crd │ │ └── custompodautoscaler.com_custompodautoscalers.yaml │ └── namespace │ │ ├── operator.yaml │ │ ├── role.yaml │ │ ├── role_binding.yaml │ │ └── service_account.yaml └── values.yaml ├── main.go ├── reconcile ├── reconcile.go └── reconcile_test.go └── tools.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Deploy '...' 16 | 2. Run '...' 17 | 3. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Kubernetes Details (`kubectl version`):** 23 | Kubernetes version, kubectl version etc. 24 | 25 | **Custom Pod Autoscaler Operator Details:** 26 | Version of the operator you are using. 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | push: 4 | pull_request: 5 | release: 6 | types: [created] 7 | jobs: 8 | build: 9 | name: Build 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Set up Go 13 | uses: actions/setup-go@v1 14 | with: 15 | go-version: 1.21 16 | id: go 17 | 18 | - uses: azure/setup-helm@v1 19 | with: 20 | version: 'v3.9.0' 21 | id: helm 22 | 23 | - name: Check out code into the Go module directory 24 | uses: actions/checkout@v1 25 | 26 | - name: Get dependencies 27 | run: go get -v -t -d ./... 28 | 29 | - name: Lint, unit test and build 30 | run: | 31 | export PATH=$PATH:$(go env GOPATH)/bin 32 | 33 | make vendor_modules 34 | 35 | # Run code generation 36 | make generate 37 | # Lint and format the codebase 38 | make lint 39 | make format 40 | # Check diff for changes in committed code and generated/linted code 41 | git diff --exit-code 42 | 43 | make test 44 | 45 | if [ ${{ github.event_name }} == "release" ]; then 46 | # github.ref is in the form refs/tags/VERSION, so apply regex to just get version 47 | VERSION=$(echo "${{ github.ref }}" | grep -P '([^\/]+$)' -o) 48 | else 49 | VERSION=$(git rev-parse --short ${{ github.sha }}) 50 | fi 51 | 52 | make docker VERSION=${VERSION} 53 | 54 | - name: Deploy 55 | env: 56 | DOCKER_USER: ${{ secrets.DOCKER_USER }} 57 | DOCKER_PASS: ${{ secrets.DOCKER_PASS }} 58 | if: github.event_name != 'pull_request' && github.repository == 'jthomperoo/custom-pod-autoscaler-operator' 59 | run: | 60 | if [ ${{ github.event_name }} == "release" ]; then 61 | # github.ref is in the form refs/tags/VERSION, so apply regex to just get version 62 | VERSION=$(echo "${{ github.ref }}" | grep -P '([^\/]+$)' -o) 63 | else 64 | VERSION=$(git rev-parse --short ${{ github.sha }}) 65 | fi 66 | echo "$DOCKER_PASS" | docker login --username=$DOCKER_USER --password-stdin 67 | docker push custompodautoscaler/operator:${VERSION} 68 | if [ ${{ github.event_name }} == "release" ]; then 69 | docker tag custompodautoscaler/operator:${VERSION} custompodautoscaler/operator:latest 70 | docker push custompodautoscaler/operator:latest 71 | fi 72 | 73 | - name: Bundle YAML config 74 | if: github.event_name == 'release' && github.repository == 'jthomperoo/custom-pod-autoscaler-operator' 75 | run: | 76 | # Variables to sub into k8s templates 77 | if [ ${{ github.event_name }} == "release" ]; then 78 | # github.ref is in the form refs/tags/VERSION, so apply regex to just get version 79 | VERSION=$(echo "${{ github.ref }}" | grep -P '([^\/]+$)' -o) 80 | else 81 | VERSION=$(git rev-parse --short ${{ github.sha }}) 82 | fi 83 | export VERSION=${VERSION} 84 | 85 | sed -i "/version: 0.0.0/c\version: ${VERSION}" helm/Chart.yaml 86 | helm package helm/ 87 | helm template helm/ --set mode=cluster > cluster.yaml 88 | helm template helm/ --set mode=namespaced > namespaced.yaml 89 | 90 | - name: Deploy helm package 91 | if: github.event_name == 'release' && github.repository == 'jthomperoo/custom-pod-autoscaler-operator' 92 | uses: Shopify/upload-to-release@1.0.0 93 | with: 94 | name: custom-pod-autoscaler-operator-${{ github.event.release.tag_name }}.tgz 95 | path: custom-pod-autoscaler-operator-${{ github.event.release.tag_name }}.tgz 96 | repo-token: ${{ secrets.GITHUB_TOKEN }} 97 | 98 | - name: Deploy cluster-scope YAML config 99 | if: github.event_name == 'release' && github.repository == 'jthomperoo/custom-pod-autoscaler-operator' 100 | uses: Shopify/upload-to-release@1.0.0 101 | with: 102 | name: cluster.yaml 103 | path: cluster.yaml 104 | repo-token: ${{ secrets.GITHUB_TOKEN }} 105 | 106 | - name: Deploy namespace-scope YAML config 107 | if: github.event_name == 'release' && github.repository == 'jthomperoo/custom-pod-autoscaler-operator' 108 | uses: Shopify/upload-to-release@1.0.0 109 | with: 110 | name: namespaced.yaml 111 | path: namespaced.yaml 112 | repo-token: ${{ secrets.GITHUB_TOKEN }} 113 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | vendor/ 3 | 4 | # Temporary Build Files 5 | build/_output 6 | build/_test 7 | # Created by https://www.gitignore.io/api/go,vim,emacs,visualstudiocode 8 | ### Emacs ### 9 | # -*- mode: gitignore; -*- 10 | *~ 11 | \#*\# 12 | /.emacs.desktop 13 | /.emacs.desktop.lock 14 | *.elc 15 | auto-save-list 16 | tramp 17 | .\#* 18 | # Org-mode 19 | .org-id-locations 20 | *_archive 21 | # flymake-mode 22 | *_flymake.* 23 | # eshell files 24 | /eshell/history 25 | /eshell/lastdir 26 | # elpa packages 27 | /elpa/ 28 | # reftex files 29 | *.rel 30 | # AUCTeX auto folder 31 | /auto/ 32 | # cask packages 33 | .cask/ 34 | dist/ 35 | # Flycheck 36 | flycheck_*.el 37 | # server auth directory 38 | /server/ 39 | # projectiles files 40 | .projectile 41 | projectile-bookmarks.eld 42 | # directory configuration 43 | .dir-locals.el 44 | # saveplace 45 | places 46 | # url cache 47 | url/cache/ 48 | # cedet 49 | ede-projects.el 50 | # smex 51 | smex-items 52 | # company-statistics 53 | company-statistics-cache.el 54 | # anaconda-mode 55 | anaconda-mode/ 56 | ### Go ### 57 | # Binaries for programs and plugins 58 | *.exe 59 | *.exe~ 60 | *.dll 61 | *.so 62 | *.dylib 63 | # Test binary, build with 'go test -c' 64 | *.test 65 | # Output of the go coverage tool, specifically when used with LiteIDE 66 | *.out 67 | ### Vim ### 68 | # swap 69 | .sw[a-p] 70 | .*.sw[a-p] 71 | # session 72 | Session.vim 73 | # temporary 74 | .netrwhist 75 | # auto-generated tag files 76 | tags 77 | ### VisualStudioCode ### 78 | .vscode/* 79 | .history 80 | # End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode 81 | /dist 82 | *.out 83 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [v1.4.2] - 2024-02-10 10 | ### Changed 11 | - Updated underlying dependencies to latest versions, including security fixes. 12 | 13 | ## [v1.4.1] - 2024-02-10 14 | ### Fixed 15 | - Bug that caused foreground cascade deletion to send the operator into a loop 16 | ([#110](https://github.com/jthomperoo/custom-pod-autoscaler-operator/issues/110)). 17 | ### Changed 18 | - Added Kind information into logging messages to help debug which resources are being reconciled. 19 | 20 | ## [v1.4.0] - 2023-08-13 21 | ### Added 22 | - Ability to pause autoscaling at a specific replica count. 23 | ### Changed 24 | - Upgraded to Go `v1.21`. 25 | - Upgraded package dependencies. 26 | 27 | ## [v1.3.0] - 2022-07-08 28 | ### Fixed 29 | - Bug that pod metadata was not preserved when creating the pod ([#87](https://github.com/jthomperoo/custom-pod-autoscaler-operator/issues/87)). 30 | ### Changed 31 | - Each resource provisioned by the CPA will now have the label `v1.custompodautoscaler.com/owned-by` which will 32 | contain the name of the CPA that owns the resource. This is used to help look up resources and link them back to 33 | the owned CPA. This allows the managed Pod to change name, and the operator will know to delete the old Pod when it 34 | provisions the new Pod. Addresses [#95](https://github.com/jthomperoo/custom-pod-autoscaler-operator/issues/95). 35 | 36 | ## [v1.2.1] - 2022-04-17 37 | ### Fixed 38 | - Fixed issue with namespaced deploys not working due to invalid permissions when watching resources in a namespace. 39 | 40 | ## [v1.2.0] - 2021-12-27 41 | ### Added 42 | - Support for Argo Rollouts, new option `roleRequiresArgoRollouts` which will add the required Role permissions to 43 | manage a `argoproj.io/v1alpha1` `Rollout`. 44 | 45 | ## [v1.1.1] - 2021-06-20 46 | ### Fixed 47 | - Bug that when provisioning a role that requires metrics server access (`roleRequiresMetricsServer`) the operator 48 | would not provision a role that had permission to access custom or external metrics. 49 | 50 | ## [v1.1.0] - 2021-04-07 51 | ### Added 52 | - New `roleRequiresMetricsServer` (defaults to `false`), if set to `true` the provisioned role will include permission 53 | to acess the Kubernetes metrics server. 54 | 55 | ## [v1.0.3] - 2021-03-17 56 | ### Changed 57 | - Upgrade to stable Operator SDK version `v1.5.0`. 58 | ### Fixed 59 | - Bug that did not allow the operator to install correctly on K8s >= `v1.18.x` 60 | 61 | ## [v1.0.2] - 2020-09-13 62 | ### Fixed 63 | - Bug where the service account was regenerating its secrets on every reconcile, resulting in a pile up of secrets 64 | that are never garbage collected. Service accounts now retain secrets between reconciles. 65 | 66 | ## [v1.0.1] - 2020-08-15 67 | ### Fixed 68 | - When deploying using Cluster wide scope to a namespace, the `ClusterRoleBinding` no longer only searches in the 69 | `default` namespace for the `ServiceAccount` - instead it searches in the namespace that the helm chart is deployed to. 70 | This change is only for the helm deploys, as such the kubectl deployment method is no longer recommended, as it only 71 | supports deploying to the `default` namespace for cluster wide installs. 72 | 73 | ## [v1.0.0] - 2020-07-19 74 | 75 | ## [v0.7.0] - 2020-07-18 76 | ### Changed 77 | - CustomPodAutoscaler resource changed from `v1alpha` to `v1`. 78 | - Operator Docker image now using `distroless` rather than RHEL `ubi7`. 79 | - Add deployment via helm. 80 | 81 | ## [v0.6.0] - 2020-06-24 82 | ### Added 83 | - New options for deciding if a resource should be provisioned by the CPAO, or if they are provided already by the 84 | user/another system. All provision options default to `true`. 85 | - `provisionRole` - determines if a `Role` should be provisioned. 86 | - `provisionRoleBinding` - determines if a `RoleBinding` should be provisioned. 87 | - `provisionServiceAccount` - determines if a `ServiceAccount` should beprovisioned. 88 | - `provisionPod` - determines if a `Pod` should be provisioned. 89 | - Resources can now be updated at runtime, without deleting and recreating the CPA. 90 | - All resources will be updated using the standard K8s Update procedure, except for `Pod` resources, which will be 91 | deleted and recreated, in order to use any new image provided. 92 | 93 | ## [v0.5.0] - 2020-01-18 94 | ### Added 95 | - Add permissions to role for managing ReplicationControllers, ReplicaSets, and StatefulSets. 96 | - Add permissions to use scaling API. 97 | - When a resource already exists, the operator will check if the assigned CPA has been set as its owner; if it isn't it 98 | will set it, if not it will skip it. This can be used by CPAs to modify the resources for the CPA. 99 | 100 | ## [0.4.0] - 2019-11-16 101 | ### Changed 102 | - Use ScaleTargetRef rather than a label selector to choose which resource to manage, consistent with Horizontal Pod 103 | Autoscaler. 104 | - Can now define a PodSpec rather than a Docker image. 105 | ### Removed 106 | - PullPolicy removed as it can now be defined in the template PodSpec. 107 | - Image removed as it is now defined within a PodSpec instead. 108 | 109 | ## [0.3.0] - 2019-11-03 110 | ### Changed 111 | - Deliver configuration via environment variables in a consistent format with YAML config, all lowercase. 112 | 113 | ## [0.2.1] - 2019-10-30 114 | ### Fixed 115 | - Update operator YAML for cluster install to point to correct service account. 116 | 117 | ## [0.2.0] - 2019-10-28 118 | ### Added 119 | - Allow single line install of operator. 120 | - Allow specifying selector in custom resource defintion file. 121 | - Allow operator to run either cluster-scoped or namespace-scoped. 122 | - Create service account, role and role binding for CPA. 123 | - Support creating a CPA in a namespace. 124 | ### Changed 125 | - Added permissions for operator to manage roles and role bindings in operator role definition. 126 | 127 | ## [0.1.0] - 2019-09-28 128 | ### Added 129 | - Allow use of config.yaml to select deployment to manage. 130 | - Provide YAML config for Custom Pod Autoscaler custom resource. 131 | - Provide YAML config for resources required to run operator. 132 | - Provide controller that provisions a single pod deployment to run CPA in. 133 | - Allow creation/deletion of CPA. 134 | 135 | [Unreleased]: 136 | https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/v1.4.2...HEAD 137 | [v1.4.2]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/v1.4.1...v1.4.2 138 | [v1.4.1]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/v1.4.0...v1.4.1 139 | [v1.4.0]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/v1.3.0...v1.4.0 140 | [v1.3.0]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/v1.2.1...v1.3.0 141 | [v1.2.1]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/v1.2.0...v1.2.1 142 | [v1.2.0]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/v1.1.1...v1.2.0 143 | [v1.1.1]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/v1.1.0...v1.1.1 144 | [v1.1.0]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/v1.0.3...v1.1.0 145 | [v1.0.3]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/v1.0.2...v1.0.3 146 | [v1.0.2]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/v1.0.1...v1.0.2 147 | [v1.0.1]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/v1.0.0...v1.0.1 148 | [v1.0.0]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/v0.7.0...v1.0.0 149 | [v0.7.0]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/v0.6.0...v0.7.0 150 | [v0.6.0]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/v0.5.0...v0.6.0 151 | [v0.5.0]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/0.4.0...v0.5.0 152 | [0.4.0]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/0.3.0...0.4.0 153 | [0.3.0]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/0.2.1...0.3.0 154 | [0.2.1]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/0.2.0...0.2.1 155 | [0.2.0]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/compare/0.1.0...0.2.0 156 | [0.1.0]: https://github.com/jthomperoo/custom-pod-autoscaler-operator/releases/tag/0.1.0 157 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct - Custom Pod Autoscaler Operator 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to a positive environment for our 15 | community include: 16 | 17 | * Demonstrating empathy and kindness toward other people 18 | * Being respectful of differing opinions, viewpoints, and experiences 19 | * Giving and gracefully accepting constructive feedback 20 | * Accepting responsibility and apologizing to those affected by our mistakes, 21 | and learning from the experience 22 | * Focusing on what is best not just for us as individuals, but for the 23 | overall community 24 | 25 | Examples of unacceptable behavior include: 26 | 27 | * The use of sexualized language or imagery, and sexual attention or 28 | advances 29 | * Trolling, insulting or derogatory comments, and personal or political attacks 30 | * Public or private harassment 31 | * Publishing others' private information, such as a physical or email 32 | address, without their explicit permission 33 | * Other conduct which could reasonably be considered inappropriate in a 34 | professional setting 35 | 36 | ## Our Responsibilities 37 | 38 | Project maintainers are responsible for clarifying and enforcing our standards of 39 | acceptable behavior and will take appropriate and fair corrective action in 40 | response to any instances of unacceptable behavior. 41 | 42 | Project maintainers have the right and responsibility to remove, edit, or reject 43 | comments, commits, code, wiki edits, issues, and other contributions that are 44 | not aligned to this Code of Conduct, or to ban 45 | temporarily or permanently any contributor for other behaviors that they deem 46 | inappropriate, threatening, offensive, or harmful. 47 | 48 | ## Scope 49 | 50 | This Code of Conduct applies within all community spaces, and also applies when 51 | an individual is officially representing the community in public spaces. 52 | Examples of representing our community include using an official e-mail address, 53 | posting via an official social media account, or acting as an appointed 54 | representative at an online or offline event. 55 | 56 | ## Enforcement 57 | 58 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 59 | reported to the community leaders responsible for enforcement at j.thomperoo@hotmail.com. 60 | All complaints will be reviewed and investigated promptly and fairly. 61 | 62 | All community leaders are obligated to respect the privacy and security of the 63 | reporter of any incident. 64 | 65 | ## Attribution 66 | 67 | This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org/), version 68 | [1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct/code_of_conduct.md) and 69 | [2.0](https://www.contributor-covenant.org/version/2/0/code_of_conduct/code_of_conduct.md), 70 | and was generated by [contributing-gen](https://github.com/bttger/contributing-gen). 71 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing to Custom Pod Autoscaler Operator 3 | 4 | First off, thanks for taking the time to contribute! ❤️ 5 | 6 | All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways 7 | to help and details about how this project handles them. Please make sure to read the relevant section before making 8 | your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. 9 | The community looks forward to your contributions. 🎉 10 | 11 | > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support 12 | > the project and show your appreciation, which we would also be very happy about: 13 | > - Star the project 14 | > - Tweet about it 15 | > - Refer this project in your project's readme 16 | > - Mention the project at local meetups and tell your friends/colleagues 17 | 18 | ## Table of Contents 19 | 20 | - [Code of Conduct](#code-of-conduct) 21 | - [I Have a Question](#i-have-a-question) 22 | - [I Want To Contribute](#i-want-to-contribute) 23 | - [Reporting Bugs](#reporting-bugs) 24 | - [Suggesting Enhancements](#suggesting-enhancements) 25 | 26 | ## Code of Conduct 27 | 28 | This project and everyone participating in it is governed by the 29 | [Custom Pod Autoscaler Operator Code of Conduct](https://github.com/jthomperoo/custom-pod-autoscaler-operator/blob/master/CODE_OF_CONDUCT.md). 30 | By participating, you are expected to uphold this code. Please report unacceptable behavior 31 | to j.thomperoo@hotmail.com. 32 | 33 | ## I Have a Question 34 | 35 | > If you want to ask a question, we assume that you have read the available 36 | > [Documentation](https://custom-pod-autoscaler.readthedocs.io/en/latest/). 37 | Before you ask a question, it is best to search for existing 38 | [Issues](https://github.com/jthomperoo/custom-pod-autoscaler-operator/issues) that might help you. In case you have 39 | found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to 40 | search the internet for answers first. 41 | 42 | If you then still feel the need to ask a question and need clarification, we recommend the following: 43 | 44 | - Open an [Issue](https://github.com/jthomperoo/custom-pod-autoscaler-operator/issues/new). 45 | - Provide as much context as you can about what you're running into. 46 | - Provide project and platform versions, depending on what seems relevant. 47 | 48 | We will then take care of the issue as soon as possible. 49 | 50 | ## I Want To Contribute 51 | 52 | > ### Legal Notice 53 | > When contributing to this project, you must agree that you have authored 100% of the content, that you have the 54 | > necessary rights to the content and that the content you contribute may be provided under the project license. 55 | ### Reporting Bugs 56 | 57 | 58 | #### Before Submitting a Bug Report 59 | 60 | A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to 61 | investigate carefully, collect information and describe the issue in detail in your report. Please complete the 62 | following steps in advance to help us fix any potential bug as fast as possible. 63 | 64 | - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment 65 | components/versions (Make sure that you have read the 66 | [documentation](https://custom-pod-autoscaler.readthedocs.io/en/latest/). If you are looking for support, you might 67 | want to check [this section](#i-have-a-question)). 68 | - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there 69 | is not already a bug report existing for your bug or error in the 70 | [bug tracker](https://github.com/jthomperoo/custom-pod-autoscaler-operator/issues). 71 | - Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have 72 | discussed the issue. 73 | - Collect information about the bug: 74 | - Stack trace (Traceback) 75 | - OS, Platform and Version (Windows, Linux, macOS, x86, ARM) 76 | - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. 77 | - Possibly your input and the output 78 | - Can you reliably reproduce the issue? And can you also reproduce it with older versions? 79 | 80 | 81 | #### How Do I Submit a Good Bug Report? 82 | 83 | We use GitHub issues to track bugs and errors. If you run into an issue with the project: 84 | 85 | - Open an [Issue](https://github.com/jthomperoo/custom-pod-autoscaler-operator/issues/new). (Since we can't be sure at 86 | this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) 87 | - Explain the behavior you would expect and the actual behavior. 88 | - Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to 89 | recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem 90 | and create a reduced test case. 91 | - Provide the information you collected in the previous section. 92 | 93 | Once it's filed: 94 | 95 | - A team member will try to reproduce the issue with your provided steps. 96 | - If the team is able to reproduce the issue the issue will be left to be 97 | [implemented by someone](#your-first-code-contribution). 98 | 99 | ### Suggesting Enhancements 100 | 101 | This section guides you through submitting an enhancement suggestion for Custom Pod Autoscaler Operator, 102 | **including completely new features and minor improvements to existing functionality**. Following these guidelines will 103 | help maintainers and the community to understand your suggestion and find related suggestions. 104 | 105 | 106 | #### Before Submitting an Enhancement 107 | 108 | - Make sure that you are using the latest version. 109 | - Read the [documentation](https://custom-pod-autoscaler.readthedocs.io/en/latest/) carefully and find out if the 110 | functionality is already covered, maybe by an individual configuration. 111 | - Perform a [search](https://github.com/jthomperoo/custom-pod-autoscaler-operator/issues) to see if the enhancement has 112 | already been suggested. If it has, add a comment to the existing issue instead of opening a new one. 113 | - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to 114 | convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful 115 | to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider 116 | writing an add-on/plugin library. 117 | 118 | 119 | #### How Do I Submit a Good Enhancement Suggestion? 120 | 121 | Enhancement suggestions are tracked as 122 | [GitHub issues](https://github.com/jthomperoo/custom-pod-autoscaler-operator/issues). 123 | 124 | - Use a **clear and descriptive title** for the issue to identify the suggestion. 125 | - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. 126 | - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point 127 | you can also tell which alternatives do not work for you. 128 | - **Explain why this enhancement would be useful** to most Custom Pod Autoscaler Operator users. You may also want to 129 | point out the other projects that solved it better and which could serve as inspiration. 130 | 131 | ## Styleguides 132 | 133 | ### Commit messages 134 | 135 | Commit messages should follow the ['How to Write a Git Commit Message'](https://chris.beams.io/posts/git-commit/) guide. 136 | 137 | ### Documentation 138 | 139 | Documentation should be in plain english, with 120 character max line width. 140 | 141 | ### Code 142 | 143 | Project code should pass the linter and all tests should pass. 144 | 145 | ## Attribution 146 | This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)! 147 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Custom Pod Autoscaler Authors. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Use distroless as minimal base image to package the manager binary 16 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 17 | FROM gcr.io/distroless/static:nonroot 18 | WORKDIR / 19 | COPY dist/ . 20 | USER nonroot:nonroot 21 | 22 | ENTRYPOINT ["/operator"] 23 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Helm 4 | 5 | ### Cluster scoped install 6 | Run this to install the Operator and Custom Pod Autoscaler definition with 7 | cluster-wide scope on your cluster: 8 | 9 | ``` 10 | VERSION=v1.4.2 11 | HELM_CHART=custom-pod-autoscaler-operator 12 | helm install ${HELM_CHART} https://github.com/jthomperoo/custom-pod-autoscaler-operator/releases/download/${VERSION}/custom-pod-autoscaler-operator-${VERSION}.tgz 13 | ``` 14 | 15 | ### Namespace scoped install 16 | Run this to install the Operator and Custom Pod Autoscaler definition with 17 | namespaced scope on your cluster: 18 | 19 | ``` 20 | NAMESPACE= 21 | VERSION=v1.4.2 22 | HELM_CHART=custom-pod-autoscaler-operator 23 | helm install --set mode=namespaced --namespace=${NAMESPACE} ${HELM_CHART} https://github.com/jthomperoo/custom-pod-autoscaler-operator/releases/download/${VERSION}/custom-pod-autoscaler-operator-${VERSION}.tgz 24 | ``` 25 | 26 | ## Kubectl 27 | 28 | ### Cluster scoped install 29 | 30 | > Installation this using kubectl method only supports installation into the 31 | > 'default' namespace when doing a cluster wide install. 32 | > This is due to a limitation in how ClusterRoleBindings link to 33 | > ServiceAccounts, to deploy to any namespace please use the helm deployment 34 | > method. 35 | 36 | Run this to install the Operator and Custom Pod Autoscaler definition with 37 | cluster-wide scope on your cluster: 38 | 39 | ``` 40 | VERSION=v1.4.2 41 | kubectl apply -f https://github.com/jthomperoo/custom-pod-autoscaler-operator/releases/download/${VERSION}/cluster.yaml 42 | ``` 43 | 44 | ### Namespace scoped install 45 | Run this to install the Operator and Custom Pod Autoscaler definition with 46 | namespaced scope on your cluster: 47 | 48 | ``` 49 | NAMESPACE= 50 | VERSION=v1.4.2 51 | kubectl config set-context --current --namespace=${NAMESPACE} 52 | kubectl apply -f https://github.com/jthomperoo/custom-pod-autoscaler-operator/releases/download/${VERSION}/namespaced.yaml 53 | ``` 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REGISTRY = custompodautoscaler 2 | NAME = operator 3 | VERSION = latest 4 | 5 | default: vendor_modules generate 6 | @echo "=============Building=============" 7 | CGO_ENABLED=0 GOOS=linux go build -mod vendor -o dist/$(NAME) main.go 8 | cp LICENSE dist/LICENSE 9 | 10 | # Run linting with golint 11 | lint: vendor_modules generate 12 | @echo "=============Linting=============" 13 | go run honnef.co/go/tools/cmd/staticcheck ./... 14 | 15 | format: vendor_modules 16 | @echo "=============Formatting=============" 17 | gofmt -s -w . 18 | go mod tidy 19 | 20 | # Run tests 21 | test: vendor_modules generate 22 | @echo "=============Running tests=============" 23 | go test -mod vendor ./... -cover -coverprofile unit_cover.out 24 | 25 | # Build the docker image 26 | docker: default 27 | docker build . -t $(REGISTRY)/$(NAME):$(VERSION) 28 | 29 | # Generate code and manifests 30 | generate: get_controller-gen 31 | @echo "=============Generating Golang and YAML=============" 32 | controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..." 33 | controller-gen rbac:roleName=manager-role webhook crd paths="./..." output:crd:artifacts:config=helm/templates/crd 34 | 35 | vendor_modules: 36 | go mod vendor 37 | 38 | view_coverage: 39 | @echo "=============Loading coverage HTML=============" 40 | go tool cover -html=unit_cover.out 41 | 42 | get_controller-gen: 43 | go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.14.0 44 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: custompodautoscaler.com 2 | layout: go.kubebuilder.io/v2 3 | projectName: custom-pod-autoscaler-operator 4 | repo: github.com/jthomperoo/custom-pod-autoscaler-operator 5 | resources: 6 | - group: custompodautoscaler.com 7 | kind: CustomResourceDefinition 8 | version: v1 9 | version: 3-alpha 10 | plugins: 11 | manifests.sdk.operatorframework.io/v2: {} 12 | scorecard.sdk.operatorframework.io/v2: {} 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/jthomperoo/custom-pod-autoscaler-operator/workflows/main/badge.svg)](https://github.com/jthomperoo/custom-pod-autoscaler-operator/actions) 2 | [![go.dev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/jthomperoo/custom-pod-autoscaler-operator) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/jthomperoo/custom-pod-autoscaler-operator)](https://goreportcard.com/report/github.com/jthomperoo/custom-pod-autoscaler-operator) 4 | [![License](https://img.shields.io/:license-apache-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) 5 | 6 | # Custom Pod Autoscaler Operator 7 | 8 | This is the operator for managing [Custom Pod Autoscalers](https://github.com/jthomperoo/custom-pod-autoscaler) (CPA). 9 | This allows you to add your own CPAs to the cluster to manage autoscaling deployments, enabling this is a requirement 10 | before you can add your own CPAs. 11 | 12 | ## Installation 13 | 14 | See the [install guide](INSTALL.md) to see more in depth installation options, such as namespace specific installs and 15 | installation using kubectl. 16 | 17 | ### Quick start 18 | 19 | Run this to install the Operator and Custom Pod Autoscaler definition with cluster-wide scope on your cluster: 20 | ``` 21 | VERSION=v1.4.2 22 | HELM_CHART=custom-pod-autoscaler-operator 23 | helm install ${HELM_CHART} https://github.com/jthomperoo/custom-pod-autoscaler-operator/releases/download/${VERSION}/custom-pod-autoscaler-operator-${VERSION}.tgz 24 | ``` 25 | 26 | ## Usage 27 | 28 | See the [usage guide](USAGE.md) to see some simple usage options. For more indepth examples, check out the 29 | [Custom Pod Autoscaler repo](https://github.com/jthomperoo/custom-pod-autoscaler). 30 | 31 | ## Developing 32 | 33 | Developing this project requires these dependencies: 34 | 35 | * [Go](https://golang.org/doc/install) == `1.18` 36 | 37 | See the [contributing guide](CONTRIBUTING.md) for more information about how you can develop and contribute to this 38 | project. 39 | 40 | ## Commands 41 | 42 | * `make` - builds the operator binary. 43 | * `make docker` - build the docker image for the operator. 44 | * `make lint` - lints the codebase. 45 | * `make format` - formats the codebase, must be run to pass the CI. 46 | * `make test` - runs the Go tests. 47 | * `make generate` - generates boilerplate and YAML config for the operator. 48 | * `make view_coverage` - opens up any generated coverage reports in the browser. 49 | -------------------------------------------------------------------------------- /USAGE.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | This will show the basic usage of the Custom Pod Autoscaler Operator, for more 4 | indepth examples check out the 5 | [Custom Pod Autoscaler repo](https://github.com/jthomperoo/custom-pod-autoscaler). 6 | 7 | ## Simple Custom Pod Autoscaler 8 | 9 | ```yaml 10 | apiVersion: custompodautoscaler.com/v1 11 | kind: CustomPodAutoscaler 12 | metadata: 13 | name: python-custom-autoscaler 14 | spec: 15 | template: 16 | spec: 17 | containers: 18 | - name: python-custom-autoscaler 19 | image: python-custom-autoscaler:latest 20 | imagePullPolicy: Always 21 | scaleTargetRef: 22 | apiVersion: apps/v1 23 | kind: Deployment 24 | name: hello-kubernetes 25 | config: 26 | - name: interval 27 | value: "10000" 28 | ``` 29 | 30 | This is a simple Custom Pod Autoscaler, using an image called 31 | `python-custom-autoscaler:latest`. 32 | 33 | It provides the configuration option `interval` with a value of `10000` as an 34 | environment variable injected into the container. 35 | 36 | The target of this CPA is defined by `scaleTargetRef` - it targets a `Deployment` 37 | called `hello-kubernetes`. 38 | 39 | For more indepth examples check out the 40 | [Custom Pod Autoscaler repo](https://github.com/jthomperoo/custom-pod-autoscaler). 41 | 42 | ## Using Custom Resources 43 | 44 | ```yaml 45 | apiVersion: v1 46 | kind: ServiceAccount 47 | metadata: 48 | name: python-custom-autoscaler 49 | annotations: 50 | myCustomAnnotation: test 51 | --- 52 | apiVersion: custompodautoscaler.com/v1 53 | kind: CustomPodAutoscaler 54 | metadata: 55 | name: python-custom-autoscaler 56 | spec: 57 | template: 58 | spec: 59 | containers: 60 | - name: python-custom-autoscaler 61 | image: python-custom-autoscaler:latest 62 | imagePullPolicy: Always 63 | scaleTargetRef: 64 | apiVersion: apps/v1 65 | kind: Deployment 66 | name: hello-kubernetes 67 | provisionServiceAccount: false 68 | config: 69 | - name: interval 70 | value: "10000" 71 | ``` 72 | 73 | This is a Custom Pod Autoscaler that is similar to the basic one defined above, except 74 | that it uses a custom `ServiceAccount`, with the annotation `myCustomAnnotation`. 75 | 76 | Take note of the option inside the CPA `provisionServiceAccount: false`, which informs 77 | the CPAO that the user will be providing their own `ServiceAccount`, so it should 78 | not override it with its own provisioned `ServiceAccount`. 79 | 80 | This custom resource provision is supported for all resources the CPAO manages: 81 | 82 | - `provisionRole` - determines if a `Role` should be provisioned. 83 | - `provisionRoleBinding` - determines if a `RoleBinding` should be provisioned. 84 | - `provisionServiceAccount` - determines if a `ServiceAccount` should be 85 | provisioned. 86 | - `provisionPod` - determines if a `Pod` should be provisioned. 87 | 88 | ## Automatically Provisioning a Role with Access to the Kubernetes Metrics Server 89 | 90 | > Note: this feature is only available in Custom Pod Autoscaler Operator `v1.1.0` and above 91 | 92 | ```yaml 93 | apiVersion: custompodautoscaler.com/v1 94 | kind: CustomPodAutoscaler 95 | metadata: 96 | name: python-custom-autoscaler 97 | spec: 98 | template: 99 | spec: 100 | containers: 101 | - name: python-custom-autoscaler 102 | image: python-custom-autoscaler:latest 103 | imagePullPolicy: Always 104 | scaleTargetRef: 105 | apiVersion: apps/v1 106 | kind: Deployment 107 | name: hello-kubernetes 108 | roleRequiresMetricsServer: true 109 | config: 110 | - name: interval 111 | value: "10000" 112 | ``` 113 | 114 | This is a Custom Pod Autoscaler that is similar to the ones defined above, except it provisions a role with access to 115 | the Kubernetes metrics server. 116 | 117 | Take note of the option inside the CPA `roleRequiresMetricsServer: true` which informs the CPAO that the CPA requires 118 | access to the metrics server, so the role that is provisioned should include these accesses. 119 | 120 | ## Automatically Provisioning a Role that Supports Argo Rollouts 121 | 122 | > Note: this feature is only available in Custom Pod Autoscaler Operator `v1.2.0` and above 123 | 124 | ```yaml 125 | apiVersion: custompodautoscaler.com/v1 126 | kind: CustomPodAutoscaler 127 | metadata: 128 | name: python-custom-autoscaler 129 | spec: 130 | template: 131 | spec: 132 | containers: 133 | - name: python-custom-autoscaler 134 | image: python-custom-autoscaler:latest 135 | imagePullPolicy: Always 136 | scaleTargetRef: 137 | apiVersion: apps/v1 138 | kind: Deployment 139 | name: hello-kubernetes 140 | roleRequiresArgoRollouts: true 141 | config: 142 | - name: interval 143 | value: "10000" 144 | ``` 145 | 146 | This is a Custom Pod Autoscaler that is similar to the ones defined above, except it provisions a role with access to 147 | the ability to manage [Argo Rollouts](https://argoproj.github.io/argo-rollouts/). 148 | 149 | Take not of the option inside the CPA `roleRequiresArgoRollouts: true` which informs the CPAO that the CPA requires 150 | the ability to manage Argo Rollouts, so the role that is provisioned should include these accesses. 151 | 152 | ## Pausing autoscaling 153 | 154 | > Note: this feature is only available in Custom Pod Autoscaler Operator `v1.4.0` and above 155 | 156 | If you want to disable an autoscaler from autoscaling (e.g. during maintenance) you can do so by using the 157 | `v1.custompodautoscaler.com/paused-replicas` annotation on the Custom Pod Autoscaler. 158 | 159 | When this annotation is supplied the autoscaler pod will be deleted, and the resource will be set to whatever 160 | value is set in the annotation. 161 | 162 | For example: 163 | 164 | ```yaml 165 | apiVersion: custompodautoscaler.com/v1 166 | kind: CustomPodAutoscaler 167 | metadata: 168 | name: python-custom-autoscaler 169 | annotations: 170 | "v1.custompodautoscaler.com/paused-replicas": "42" 171 | spec: 172 | template: 173 | spec: 174 | containers: 175 | - name: python-custom-autoscaler 176 | image: python-custom-autoscaler:latest 177 | imagePullPolicy: Always 178 | scaleTargetRef: 179 | apiVersion: apps/v1 180 | kind: Deployment 181 | name: hello-kubernetes 182 | config: 183 | - name: interval 184 | value: "10000" 185 | ``` 186 | 187 | This autoscaler will be paused, with the replica count for the resource being managed set to `42`. 188 | 189 | If you want to re-enable the autoscaler after, just remove the annotation. 190 | -------------------------------------------------------------------------------- /api/v1/custompodautoscaler_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Custom Pod Autoscaler Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1 18 | 19 | // Important: Run "make generate" to regenerate code after modifying this file 20 | 21 | import ( 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | 24 | autoscaling "k8s.io/api/autoscaling/v1" 25 | 26 | corev1 "k8s.io/api/core/v1" 27 | ) 28 | 29 | // CustomPodAutoscalerConfig defines the configuration options that can be passed to the CustomPodAutoscaler 30 | type CustomPodAutoscalerConfig struct { 31 | Name string `json:"name"` 32 | Value string `json:"value"` 33 | } 34 | 35 | // CustomPodAutoscalerSpec defines the desired state of CustomPodAutoscaler 36 | type CustomPodAutoscalerSpec struct { 37 | // The image of the Custom Pod Autoscaler 38 | Template PodTemplateSpec `json:"template"` 39 | // ScaleTargetRef defining what the Custom Pod Autoscaler should manage 40 | ScaleTargetRef autoscaling.CrossVersionObjectReference `json:"scaleTargetRef"` 41 | // Configuration options to be delivered as environment variables to the container 42 | Config []CustomPodAutoscalerConfig `json:"config,omitempty"` 43 | ProvisionRole *bool `json:"provisionRole,omitempty"` 44 | ProvisionRoleBinding *bool `json:"provisionRoleBinding,omitempty"` 45 | ProvisionServiceAccount *bool `json:"provisionServiceAccount,omitempty"` 46 | ProvisionPod *bool `json:"provisionPod,omitempty"` 47 | RoleRequiresMetricsServer *bool `json:"roleRequiresMetricsServer,omitempty"` 48 | RoleRequiresArgoRollouts *bool `json:"roleRequiresArgoRollouts,omitempty"` 49 | } 50 | 51 | // CustomPodAutoscalerStatus defines the observed state of CustomPodAutoscaler 52 | type CustomPodAutoscalerStatus struct{} 53 | 54 | // CustomPodAutoscaler is the Schema for the custompodautoscalers API 55 | // +kubebuilder:object:root=true 56 | // +kubebuilder:subresource:status 57 | // +kubebuilder:object:root=true 58 | // +kubebuilder:subresource:status 59 | // +kubebuilder:resource:shortName=cpa 60 | // +groupName=custompodautoscaler.com 61 | type CustomPodAutoscaler struct { 62 | metav1.TypeMeta `json:",inline"` 63 | metav1.ObjectMeta `json:"metadata,omitempty"` 64 | 65 | Spec CustomPodAutoscalerSpec `json:"spec,omitempty"` 66 | Status CustomPodAutoscalerStatus `json:"status,omitempty"` 67 | } 68 | 69 | // CustomPodAutoscalerList contains a list of CustomPodAutoscaler 70 | // +kubebuilder:object:root=true 71 | type CustomPodAutoscalerList struct { 72 | metav1.TypeMeta `json:",inline"` 73 | metav1.ListMeta `json:"metadata,omitempty"` 74 | Items []CustomPodAutoscaler `json:"items"` 75 | } 76 | 77 | type PodTemplateSpec struct { 78 | // Standard object's metadata. 79 | // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata 80 | // +optional 81 | ObjectMeta PodMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` 82 | 83 | // Specification of the desired behavior of the pod. 84 | // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status 85 | // +optional 86 | Spec PodSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` 87 | } 88 | 89 | // +kubebuilder:pruning:PreserveUnknownFields 90 | type PodMeta metav1.ObjectMeta 91 | 92 | type PodSpec corev1.PodSpec 93 | 94 | func init() { 95 | SchemeBuilder.Register(&CustomPodAutoscaler{}, &CustomPodAutoscalerList{}) 96 | } 97 | -------------------------------------------------------------------------------- /api/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Custom Pod Autoscaler Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1 contains API Schema definitions for the custompodautoscaler.com v1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=custompodautoscaler.com 20 | package v1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "custompodautoscaler.com", Version: "v1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Copyright 2021 The Custom Pod Autoscaler Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1 22 | 23 | import ( 24 | corev1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | runtime "k8s.io/apimachinery/pkg/runtime" 27 | ) 28 | 29 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 30 | func (in *CustomPodAutoscaler) DeepCopyInto(out *CustomPodAutoscaler) { 31 | *out = *in 32 | out.TypeMeta = in.TypeMeta 33 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 34 | in.Spec.DeepCopyInto(&out.Spec) 35 | out.Status = in.Status 36 | } 37 | 38 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomPodAutoscaler. 39 | func (in *CustomPodAutoscaler) DeepCopy() *CustomPodAutoscaler { 40 | if in == nil { 41 | return nil 42 | } 43 | out := new(CustomPodAutoscaler) 44 | in.DeepCopyInto(out) 45 | return out 46 | } 47 | 48 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 49 | func (in *CustomPodAutoscaler) DeepCopyObject() runtime.Object { 50 | if c := in.DeepCopy(); c != nil { 51 | return c 52 | } 53 | return nil 54 | } 55 | 56 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 57 | func (in *CustomPodAutoscalerConfig) DeepCopyInto(out *CustomPodAutoscalerConfig) { 58 | *out = *in 59 | } 60 | 61 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomPodAutoscalerConfig. 62 | func (in *CustomPodAutoscalerConfig) DeepCopy() *CustomPodAutoscalerConfig { 63 | if in == nil { 64 | return nil 65 | } 66 | out := new(CustomPodAutoscalerConfig) 67 | in.DeepCopyInto(out) 68 | return out 69 | } 70 | 71 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 72 | func (in *CustomPodAutoscalerList) DeepCopyInto(out *CustomPodAutoscalerList) { 73 | *out = *in 74 | out.TypeMeta = in.TypeMeta 75 | in.ListMeta.DeepCopyInto(&out.ListMeta) 76 | if in.Items != nil { 77 | in, out := &in.Items, &out.Items 78 | *out = make([]CustomPodAutoscaler, len(*in)) 79 | for i := range *in { 80 | (*in)[i].DeepCopyInto(&(*out)[i]) 81 | } 82 | } 83 | } 84 | 85 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomPodAutoscalerList. 86 | func (in *CustomPodAutoscalerList) DeepCopy() *CustomPodAutoscalerList { 87 | if in == nil { 88 | return nil 89 | } 90 | out := new(CustomPodAutoscalerList) 91 | in.DeepCopyInto(out) 92 | return out 93 | } 94 | 95 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 96 | func (in *CustomPodAutoscalerList) DeepCopyObject() runtime.Object { 97 | if c := in.DeepCopy(); c != nil { 98 | return c 99 | } 100 | return nil 101 | } 102 | 103 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 104 | func (in *CustomPodAutoscalerSpec) DeepCopyInto(out *CustomPodAutoscalerSpec) { 105 | *out = *in 106 | in.Template.DeepCopyInto(&out.Template) 107 | out.ScaleTargetRef = in.ScaleTargetRef 108 | if in.Config != nil { 109 | in, out := &in.Config, &out.Config 110 | *out = make([]CustomPodAutoscalerConfig, len(*in)) 111 | copy(*out, *in) 112 | } 113 | if in.ProvisionRole != nil { 114 | in, out := &in.ProvisionRole, &out.ProvisionRole 115 | *out = new(bool) 116 | **out = **in 117 | } 118 | if in.ProvisionRoleBinding != nil { 119 | in, out := &in.ProvisionRoleBinding, &out.ProvisionRoleBinding 120 | *out = new(bool) 121 | **out = **in 122 | } 123 | if in.ProvisionServiceAccount != nil { 124 | in, out := &in.ProvisionServiceAccount, &out.ProvisionServiceAccount 125 | *out = new(bool) 126 | **out = **in 127 | } 128 | if in.ProvisionPod != nil { 129 | in, out := &in.ProvisionPod, &out.ProvisionPod 130 | *out = new(bool) 131 | **out = **in 132 | } 133 | if in.RoleRequiresMetricsServer != nil { 134 | in, out := &in.RoleRequiresMetricsServer, &out.RoleRequiresMetricsServer 135 | *out = new(bool) 136 | **out = **in 137 | } 138 | if in.RoleRequiresArgoRollouts != nil { 139 | in, out := &in.RoleRequiresArgoRollouts, &out.RoleRequiresArgoRollouts 140 | *out = new(bool) 141 | **out = **in 142 | } 143 | } 144 | 145 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomPodAutoscalerSpec. 146 | func (in *CustomPodAutoscalerSpec) DeepCopy() *CustomPodAutoscalerSpec { 147 | if in == nil { 148 | return nil 149 | } 150 | out := new(CustomPodAutoscalerSpec) 151 | in.DeepCopyInto(out) 152 | return out 153 | } 154 | 155 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 156 | func (in *CustomPodAutoscalerStatus) DeepCopyInto(out *CustomPodAutoscalerStatus) { 157 | *out = *in 158 | } 159 | 160 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomPodAutoscalerStatus. 161 | func (in *CustomPodAutoscalerStatus) DeepCopy() *CustomPodAutoscalerStatus { 162 | if in == nil { 163 | return nil 164 | } 165 | out := new(CustomPodAutoscalerStatus) 166 | in.DeepCopyInto(out) 167 | return out 168 | } 169 | 170 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 171 | func (in *PodMeta) DeepCopyInto(out *PodMeta) { 172 | *out = *in 173 | in.CreationTimestamp.DeepCopyInto(&out.CreationTimestamp) 174 | if in.DeletionTimestamp != nil { 175 | in, out := &in.DeletionTimestamp, &out.DeletionTimestamp 176 | *out = (*in).DeepCopy() 177 | } 178 | if in.DeletionGracePeriodSeconds != nil { 179 | in, out := &in.DeletionGracePeriodSeconds, &out.DeletionGracePeriodSeconds 180 | *out = new(int64) 181 | **out = **in 182 | } 183 | if in.Labels != nil { 184 | in, out := &in.Labels, &out.Labels 185 | *out = make(map[string]string, len(*in)) 186 | for key, val := range *in { 187 | (*out)[key] = val 188 | } 189 | } 190 | if in.Annotations != nil { 191 | in, out := &in.Annotations, &out.Annotations 192 | *out = make(map[string]string, len(*in)) 193 | for key, val := range *in { 194 | (*out)[key] = val 195 | } 196 | } 197 | if in.OwnerReferences != nil { 198 | in, out := &in.OwnerReferences, &out.OwnerReferences 199 | *out = make([]metav1.OwnerReference, len(*in)) 200 | for i := range *in { 201 | (*in)[i].DeepCopyInto(&(*out)[i]) 202 | } 203 | } 204 | if in.Finalizers != nil { 205 | in, out := &in.Finalizers, &out.Finalizers 206 | *out = make([]string, len(*in)) 207 | copy(*out, *in) 208 | } 209 | if in.ManagedFields != nil { 210 | in, out := &in.ManagedFields, &out.ManagedFields 211 | *out = make([]metav1.ManagedFieldsEntry, len(*in)) 212 | for i := range *in { 213 | (*in)[i].DeepCopyInto(&(*out)[i]) 214 | } 215 | } 216 | } 217 | 218 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodMeta. 219 | func (in *PodMeta) DeepCopy() *PodMeta { 220 | if in == nil { 221 | return nil 222 | } 223 | out := new(PodMeta) 224 | in.DeepCopyInto(out) 225 | return out 226 | } 227 | 228 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 229 | func (in *PodSpec) DeepCopyInto(out *PodSpec) { 230 | *out = *in 231 | if in.Volumes != nil { 232 | in, out := &in.Volumes, &out.Volumes 233 | *out = make([]corev1.Volume, len(*in)) 234 | for i := range *in { 235 | (*in)[i].DeepCopyInto(&(*out)[i]) 236 | } 237 | } 238 | if in.InitContainers != nil { 239 | in, out := &in.InitContainers, &out.InitContainers 240 | *out = make([]corev1.Container, len(*in)) 241 | for i := range *in { 242 | (*in)[i].DeepCopyInto(&(*out)[i]) 243 | } 244 | } 245 | if in.Containers != nil { 246 | in, out := &in.Containers, &out.Containers 247 | *out = make([]corev1.Container, len(*in)) 248 | for i := range *in { 249 | (*in)[i].DeepCopyInto(&(*out)[i]) 250 | } 251 | } 252 | if in.EphemeralContainers != nil { 253 | in, out := &in.EphemeralContainers, &out.EphemeralContainers 254 | *out = make([]corev1.EphemeralContainer, len(*in)) 255 | for i := range *in { 256 | (*in)[i].DeepCopyInto(&(*out)[i]) 257 | } 258 | } 259 | if in.TerminationGracePeriodSeconds != nil { 260 | in, out := &in.TerminationGracePeriodSeconds, &out.TerminationGracePeriodSeconds 261 | *out = new(int64) 262 | **out = **in 263 | } 264 | if in.ActiveDeadlineSeconds != nil { 265 | in, out := &in.ActiveDeadlineSeconds, &out.ActiveDeadlineSeconds 266 | *out = new(int64) 267 | **out = **in 268 | } 269 | if in.NodeSelector != nil { 270 | in, out := &in.NodeSelector, &out.NodeSelector 271 | *out = make(map[string]string, len(*in)) 272 | for key, val := range *in { 273 | (*out)[key] = val 274 | } 275 | } 276 | if in.AutomountServiceAccountToken != nil { 277 | in, out := &in.AutomountServiceAccountToken, &out.AutomountServiceAccountToken 278 | *out = new(bool) 279 | **out = **in 280 | } 281 | if in.ShareProcessNamespace != nil { 282 | in, out := &in.ShareProcessNamespace, &out.ShareProcessNamespace 283 | *out = new(bool) 284 | **out = **in 285 | } 286 | if in.SecurityContext != nil { 287 | in, out := &in.SecurityContext, &out.SecurityContext 288 | *out = new(corev1.PodSecurityContext) 289 | (*in).DeepCopyInto(*out) 290 | } 291 | if in.ImagePullSecrets != nil { 292 | in, out := &in.ImagePullSecrets, &out.ImagePullSecrets 293 | *out = make([]corev1.LocalObjectReference, len(*in)) 294 | copy(*out, *in) 295 | } 296 | if in.Affinity != nil { 297 | in, out := &in.Affinity, &out.Affinity 298 | *out = new(corev1.Affinity) 299 | (*in).DeepCopyInto(*out) 300 | } 301 | if in.Tolerations != nil { 302 | in, out := &in.Tolerations, &out.Tolerations 303 | *out = make([]corev1.Toleration, len(*in)) 304 | for i := range *in { 305 | (*in)[i].DeepCopyInto(&(*out)[i]) 306 | } 307 | } 308 | if in.HostAliases != nil { 309 | in, out := &in.HostAliases, &out.HostAliases 310 | *out = make([]corev1.HostAlias, len(*in)) 311 | for i := range *in { 312 | (*in)[i].DeepCopyInto(&(*out)[i]) 313 | } 314 | } 315 | if in.Priority != nil { 316 | in, out := &in.Priority, &out.Priority 317 | *out = new(int32) 318 | **out = **in 319 | } 320 | if in.DNSConfig != nil { 321 | in, out := &in.DNSConfig, &out.DNSConfig 322 | *out = new(corev1.PodDNSConfig) 323 | (*in).DeepCopyInto(*out) 324 | } 325 | if in.ReadinessGates != nil { 326 | in, out := &in.ReadinessGates, &out.ReadinessGates 327 | *out = make([]corev1.PodReadinessGate, len(*in)) 328 | copy(*out, *in) 329 | } 330 | if in.RuntimeClassName != nil { 331 | in, out := &in.RuntimeClassName, &out.RuntimeClassName 332 | *out = new(string) 333 | **out = **in 334 | } 335 | if in.EnableServiceLinks != nil { 336 | in, out := &in.EnableServiceLinks, &out.EnableServiceLinks 337 | *out = new(bool) 338 | **out = **in 339 | } 340 | if in.PreemptionPolicy != nil { 341 | in, out := &in.PreemptionPolicy, &out.PreemptionPolicy 342 | *out = new(corev1.PreemptionPolicy) 343 | **out = **in 344 | } 345 | if in.Overhead != nil { 346 | in, out := &in.Overhead, &out.Overhead 347 | *out = make(corev1.ResourceList, len(*in)) 348 | for key, val := range *in { 349 | (*out)[key] = val.DeepCopy() 350 | } 351 | } 352 | if in.TopologySpreadConstraints != nil { 353 | in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints 354 | *out = make([]corev1.TopologySpreadConstraint, len(*in)) 355 | for i := range *in { 356 | (*in)[i].DeepCopyInto(&(*out)[i]) 357 | } 358 | } 359 | if in.SetHostnameAsFQDN != nil { 360 | in, out := &in.SetHostnameAsFQDN, &out.SetHostnameAsFQDN 361 | *out = new(bool) 362 | **out = **in 363 | } 364 | if in.OS != nil { 365 | in, out := &in.OS, &out.OS 366 | *out = new(corev1.PodOS) 367 | **out = **in 368 | } 369 | if in.HostUsers != nil { 370 | in, out := &in.HostUsers, &out.HostUsers 371 | *out = new(bool) 372 | **out = **in 373 | } 374 | if in.SchedulingGates != nil { 375 | in, out := &in.SchedulingGates, &out.SchedulingGates 376 | *out = make([]corev1.PodSchedulingGate, len(*in)) 377 | copy(*out, *in) 378 | } 379 | if in.ResourceClaims != nil { 380 | in, out := &in.ResourceClaims, &out.ResourceClaims 381 | *out = make([]corev1.PodResourceClaim, len(*in)) 382 | for i := range *in { 383 | (*in)[i].DeepCopyInto(&(*out)[i]) 384 | } 385 | } 386 | } 387 | 388 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodSpec. 389 | func (in *PodSpec) DeepCopy() *PodSpec { 390 | if in == nil { 391 | return nil 392 | } 393 | out := new(PodSpec) 394 | in.DeepCopyInto(out) 395 | return out 396 | } 397 | 398 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 399 | func (in *PodTemplateSpec) DeepCopyInto(out *PodTemplateSpec) { 400 | *out = *in 401 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 402 | in.Spec.DeepCopyInto(&out.Spec) 403 | } 404 | 405 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodTemplateSpec. 406 | func (in *PodTemplateSpec) DeepCopy() *PodTemplateSpec { 407 | if in == nil { 408 | return nil 409 | } 410 | out := new(PodTemplateSpec) 411 | in.DeepCopyInto(out) 412 | return out 413 | } 414 | -------------------------------------------------------------------------------- /controllers/custompodautoscaler_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Custom Pod Autoscaler Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "strconv" 22 | 23 | corev1 "k8s.io/api/core/v1" 24 | 25 | "k8s.io/apimachinery/pkg/api/errors" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/apimachinery/pkg/runtime" 28 | "k8s.io/apimachinery/pkg/runtime/schema" 29 | "k8s.io/apimachinery/pkg/util/json" 30 | 31 | "k8s.io/client-go/dynamic" 32 | "k8s.io/client-go/kubernetes" 33 | "k8s.io/client-go/rest" 34 | "k8s.io/client-go/restmapper" 35 | k8sscale "k8s.io/client-go/scale" 36 | 37 | ctrl "sigs.k8s.io/controller-runtime" 38 | "sigs.k8s.io/controller-runtime/pkg/builder" 39 | "sigs.k8s.io/controller-runtime/pkg/client" 40 | "sigs.k8s.io/controller-runtime/pkg/event" 41 | "sigs.k8s.io/controller-runtime/pkg/predicate" 42 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 43 | 44 | "github.com/go-logr/logr" 45 | 46 | custompodautoscalercomv1 "github.com/jthomperoo/custom-pod-autoscaler-operator/api/v1" 47 | rbacv1 "k8s.io/api/rbac/v1" 48 | ) 49 | 50 | const ( 51 | managedByLabel = "app.kubernetes.io/managed-by" 52 | OwnedByLabel = "v1.custompodautoscaler.com/owned-by" 53 | PausedReplicasAnnotation = "v1.custompodautoscaler.com/paused-replicas" 54 | ) 55 | 56 | type K8sReconciler interface { 57 | Reconcile( 58 | reqLogger logr.Logger, 59 | instance *custompodautoscalercomv1.CustomPodAutoscaler, 60 | obj metav1.Object, 61 | shouldProvision bool, 62 | updateable bool, 63 | kind string, 64 | ) (reconcile.Result, error) 65 | PodCleanup(reqLogger logr.Logger, instance *custompodautoscalercomv1.CustomPodAutoscaler) error 66 | } 67 | 68 | // CustomPodAutoscalerReconciler reconciles a CustomPodAutoscaler object. 69 | type CustomPodAutoscalerReconciler struct { 70 | client.Client 71 | Log logr.Logger 72 | Scheme *runtime.Scheme 73 | KubernetesResourceReconciler K8sReconciler 74 | ScalingClient k8sscale.ScalesGetter 75 | } 76 | 77 | // PrimaryPred is the predicate that filters events for the CustomPodAutoscaler primary resource. 78 | var PrimaryPred = predicate.Funcs{ 79 | UpdateFunc: func(e event.UpdateEvent) bool { 80 | return true 81 | }, 82 | DeleteFunc: func(e event.DeleteEvent) bool { 83 | return true 84 | }, 85 | CreateFunc: func(e event.CreateEvent) bool { 86 | return true 87 | }, 88 | GenericFunc: func(e event.GenericEvent) bool { 89 | return false 90 | }, 91 | } 92 | 93 | // SecondaryPred is the predicate that filters events for the CustomPodAutoscaler's secondary 94 | // resources (deployment/service/role/rolebinding). 95 | var SecondaryPred = predicate.Funcs{ 96 | UpdateFunc: func(e event.UpdateEvent) bool { 97 | return false 98 | }, 99 | DeleteFunc: func(e event.DeleteEvent) bool { 100 | return true 101 | }, 102 | CreateFunc: func(e event.CreateEvent) bool { 103 | return false 104 | }, 105 | GenericFunc: func(e event.GenericEvent) bool { 106 | return false 107 | }, 108 | } 109 | 110 | // Reconcile reads that state of the cluster for a CustomPodAutoscaler object and makes changes based on the state read 111 | // and what is in the CustomPodAutoscaler.Spec 112 | // The Controller will requeue the Request to be processed again if the returned error is non-nil or 113 | // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. 114 | func (r *CustomPodAutoscalerReconciler) Reconcile(context context.Context, req ctrl.Request) (ctrl.Result, error) { 115 | reqLogger := r.Log.WithValues("Request", req.NamespacedName) 116 | 117 | // Fetch the CustomPodAutoscaler instance 118 | instance := &custompodautoscalercomv1.CustomPodAutoscaler{} 119 | err := r.Client.Get(context, req.NamespacedName, instance) 120 | if err != nil { 121 | if errors.IsNotFound(err) { 122 | // Request object not found, could have been deleted after reconcile request. 123 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 124 | // Return and don't requeue 125 | return reconcile.Result{}, nil 126 | } 127 | // Error reading the object - requeue the request. 128 | return reconcile.Result{}, err 129 | } 130 | 131 | if instance.DeletionTimestamp != nil { 132 | reqLogger.Info("Custom Pod Autoscaler marked for deletion, ignoring reconcilation of dependencies ", "Kind", "custompodautoscaler.com/v1/CustomPodAutoscaler", "Namespace", instance.GetNamespace(), "Name", instance.GetName()) 133 | return reconcile.Result{}, nil 134 | } 135 | 136 | // Check the presence of "v1.custompodautoscaler.com/paused-replicas" annotation on the CPA pod 137 | // Pauses autoscaling (deletes autoscaling pod) and manually sets replica count of scale target 138 | // Mimics functionality of https://keda.sh/docs/2.11/concepts/scaling-deployments/#pause-autoscaling 139 | pausedReplicasCount, pausedAnnotationFound := instance.GetAnnotations()[PausedReplicasAnnotation] 140 | if pausedAnnotationFound { 141 | // Get paused replicas count from annotation metadata 142 | pausedReplicasCountInt64, err := strconv.ParseInt(pausedReplicasCount, 10, 32) 143 | pausedReplicasCountInt32 := int32(pausedReplicasCountInt64) 144 | if err != nil { 145 | return reconcile.Result{}, err 146 | } 147 | 148 | // Use the reconciler client to delete the pod that normally does the scaling 149 | // This should be done first so the autoscaler does not override 150 | // the scaling changes made by the operator 151 | if err := r.Client.Delete(context, instance); err != nil { 152 | return reconcile.Result{}, err 153 | } 154 | 155 | // scaleTargetRef is the pod or service that is being autoscaled 156 | // ScaleTargetRef{} = CrossVersionObjectReference{Kind string, Name string, APIVersion string} 157 | // https://github.com/kubernetes/api/blob/v0.27.4/autoscaling/v1/types.go 158 | scaleTargetRef := instance.Spec.ScaleTargetRef 159 | 160 | // ex. ParseGroupVersion("custompodautoscaler.com/v1") 161 | // = GroupVersion{Group: "custompodautoscaler.com", Version: "v1"} 162 | // https://github.com/kubernetes/apimachinery/blob/v0.27.3/pkg/runtime/schema/group_version.go 163 | resourceGV, err := schema.ParseGroupVersion(scaleTargetRef.APIVersion) 164 | if err != nil { 165 | return reconcile.Result{}, err 166 | } 167 | 168 | targetGR := schema.GroupResource{ 169 | Group: resourceGV.Group, // ex. "custompodautoscaler.com" 170 | Resource: scaleTargetRef.Kind, // ex. "CustomPodAutoscaler" 171 | } 172 | 173 | // Get the scale request for a resource (https://github.com/kubernetes/api/blob/v0.27.4/autoscaling/v1/types.go) 174 | // https://github.com/kubernetes/client-go/blob/master/scale/client.go 175 | scaleResource, err := r.ScalingClient.Scales(instance.Namespace).Get(context, targetGR, scaleTargetRef.Name, metav1.GetOptions{}) 176 | if err != nil { 177 | return reconcile.Result{}, err 178 | } 179 | 180 | // Set new target replicas 181 | scaleResource.Spec.Replicas = pausedReplicasCountInt32 182 | 183 | // Update the resource with new replica count 184 | // https://github.com/kubernetes/client-go/blob/master/scale/client.go 185 | _, err = r.ScalingClient.Scales(instance.Namespace).Update(context, targetGR, scaleResource, metav1.UpdateOptions{}) 186 | if err != nil { 187 | return reconcile.Result{}, err 188 | } 189 | 190 | // Return and don't requeue 191 | return reconcile.Result{}, nil 192 | } 193 | 194 | if instance.Spec.ProvisionRole == nil { 195 | defaultVal := true 196 | instance.Spec.ProvisionRole = &defaultVal 197 | } 198 | if instance.Spec.ProvisionRoleBinding == nil { 199 | defaultVal := true 200 | instance.Spec.ProvisionRoleBinding = &defaultVal 201 | } 202 | if instance.Spec.ProvisionServiceAccount == nil { 203 | defaultVal := true 204 | instance.Spec.ProvisionServiceAccount = &defaultVal 205 | } 206 | if instance.Spec.ProvisionPod == nil { 207 | defaultVal := true 208 | instance.Spec.ProvisionPod = &defaultVal 209 | } 210 | if instance.Spec.RoleRequiresMetricsServer == nil { 211 | defaultVal := false 212 | instance.Spec.RoleRequiresMetricsServer = &defaultVal 213 | } 214 | if instance.Spec.RoleRequiresArgoRollouts == nil { 215 | defaultVal := false 216 | instance.Spec.RoleRequiresArgoRollouts = &defaultVal 217 | } 218 | 219 | // Parse scaleTargetRef 220 | scaleTargetRef, err := json.Marshal(instance.Spec.ScaleTargetRef) 221 | if err != nil { 222 | // Should not occur, panic 223 | panic(err) 224 | } 225 | 226 | labels := map[string]string{ 227 | managedByLabel: "custom-pod-autoscaler-operator", 228 | OwnedByLabel: instance.Name, 229 | } 230 | 231 | // Define a new Service Account object 232 | serviceAccount := &corev1.ServiceAccount{ 233 | ObjectMeta: metav1.ObjectMeta{ 234 | Name: instance.Name, 235 | Namespace: instance.Namespace, 236 | Labels: labels, 237 | }, 238 | } 239 | 240 | result, err := r.KubernetesResourceReconciler.Reconcile(reqLogger, instance, serviceAccount, *instance.Spec.ProvisionServiceAccount, true, "v1/ServiceAccount") 241 | if err != nil { 242 | return result, err 243 | } 244 | 245 | role := &rbacv1.Role{ 246 | ObjectMeta: metav1.ObjectMeta{ 247 | Name: instance.Name, 248 | Namespace: instance.Namespace, 249 | Labels: labels, 250 | }, 251 | Rules: []rbacv1.PolicyRule{ 252 | { 253 | APIGroups: []string{""}, 254 | Resources: []string{"pods", "replicationcontrollers", "replicationcontrollers/scale"}, 255 | Verbs: []string{"*"}, 256 | }, 257 | { 258 | APIGroups: []string{"apps"}, 259 | Resources: []string{"deployments", "deployments/scale", "replicasets", "replicasets/scale", "statefulsets", "statefulsets/scale"}, 260 | Verbs: []string{"*"}, 261 | }, 262 | }, 263 | } 264 | 265 | if *instance.Spec.RoleRequiresMetricsServer { 266 | role.Rules = append(role.Rules, rbacv1.PolicyRule{ 267 | APIGroups: []string{"metrics.k8s.io", "custom.metrics.k8s.io", "external.metrics.k8s.io"}, 268 | Resources: []string{"*"}, 269 | Verbs: []string{"*"}, 270 | }) 271 | } 272 | 273 | if *instance.Spec.RoleRequiresArgoRollouts { 274 | role.Rules = append(role.Rules, rbacv1.PolicyRule{ 275 | APIGroups: []string{"argoproj.io"}, 276 | Resources: []string{"rollouts", "rollouts/scale"}, 277 | Verbs: []string{"*"}, 278 | }) 279 | } 280 | 281 | result, err = r.KubernetesResourceReconciler.Reconcile(reqLogger, instance, role, *instance.Spec.ProvisionRole, true, "v1/Role") 282 | if err != nil { 283 | return result, err 284 | } 285 | 286 | // Define a new Role Binding object 287 | roleBinding := &rbacv1.RoleBinding{ 288 | ObjectMeta: metav1.ObjectMeta{ 289 | Name: instance.Name, 290 | Namespace: instance.Namespace, 291 | Labels: labels, 292 | }, 293 | Subjects: []rbacv1.Subject{ 294 | { 295 | Kind: "ServiceAccount", 296 | Name: instance.Name, 297 | Namespace: instance.Namespace, 298 | }, 299 | }, 300 | RoleRef: rbacv1.RoleRef{ 301 | Kind: "Role", 302 | Name: instance.Name, 303 | APIGroup: "rbac.authorization.k8s.io", 304 | }, 305 | } 306 | result, err = r.KubernetesResourceReconciler.Reconcile(reqLogger, instance, roleBinding, *instance.Spec.ProvisionRoleBinding, true, "v1/RoleBinding") 307 | if err != nil { 308 | return result, err 309 | } 310 | 311 | // Set up Pod labels, if labels are provided in the template Pod Spec the labels are merged 312 | // with the CPA managed-by label, otherwise only the managed-by label is added 313 | var podLabels map[string]string 314 | if instance.Spec.Template.ObjectMeta.Labels == nil { 315 | podLabels = map[string]string{} 316 | } else { 317 | podLabels = instance.Spec.Template.ObjectMeta.Labels 318 | } 319 | podLabels[managedByLabel] = "custom-pod-autoscaler-operator" 320 | podLabels[OwnedByLabel] = instance.Name 321 | 322 | // Set up ObjectMeta, if no name or namespaces are provided in the template PodSpec then 323 | // the CPA name and namespace are used 324 | objectMeta := instance.Spec.Template.ObjectMeta 325 | if objectMeta.Name == "" { 326 | objectMeta.Name = instance.Name 327 | } 328 | if objectMeta.Namespace == "" { 329 | objectMeta.Namespace = instance.Namespace 330 | } 331 | objectMeta.Labels = podLabels 332 | 333 | // Set up the PodSpec template 334 | podSpec := instance.Spec.Template.Spec 335 | // Inject environment variables to every Container specified by the PodSpec 336 | containers := []corev1.Container{} 337 | for _, container := range podSpec.Containers { 338 | // If no environment variables specified by the template PodSpec, set up empty env vars 339 | // slice 340 | var envVars []corev1.EnvVar 341 | if container.Env == nil { 342 | envVars = []corev1.EnvVar{} 343 | } else { 344 | envVars = container.Env 345 | } 346 | // Inject in configuration, such as namespace, target ref and configuration 347 | // options as environment variables 348 | envVars = append(envVars, cpaEnvVars(instance, string(scaleTargetRef))...) 349 | container.Env = envVars 350 | containers = append(containers, container) 351 | } 352 | // Update PodSpec to use the modified containers, and to point to the provisioned service account 353 | podSpec.Containers = containers 354 | podSpec.ServiceAccountName = serviceAccount.Name 355 | 356 | // Define Pod object with ObjectMeta and modified PodSpec 357 | pod := &corev1.Pod{ 358 | ObjectMeta: metav1.ObjectMeta(objectMeta), 359 | Spec: corev1.PodSpec(podSpec), 360 | } 361 | result, err = r.KubernetesResourceReconciler.Reconcile(reqLogger, instance, pod, *instance.Spec.ProvisionPod, false, "v1/Pod") 362 | if err != nil { 363 | return result, err 364 | } 365 | 366 | // Clean up any orphaned pods (e.g. renaming pod, old pod should be deleted) 367 | err = r.KubernetesResourceReconciler.PodCleanup(reqLogger, instance) 368 | if err != nil { 369 | return result, err 370 | } 371 | 372 | return result, nil 373 | } 374 | 375 | // cpaEnvVars builds a list of environment variables from the Spec 376 | func cpaEnvVars(cr *custompodautoscalercomv1.CustomPodAutoscaler, scaleTargetRef string) []corev1.EnvVar { 377 | envVars := []corev1.EnvVar{ 378 | { 379 | Name: "scaleTargetRef", 380 | Value: scaleTargetRef, 381 | }, 382 | { 383 | Name: "namespace", 384 | Value: cr.Namespace, 385 | }, 386 | } 387 | envVars = append(envVars, createEnvVarsFromConfig(cr.Spec.Config)...) 388 | return envVars 389 | } 390 | 391 | // createEnvVarsFromConfig converts CPA config to environment variables 392 | func createEnvVarsFromConfig(configs []custompodautoscalercomv1.CustomPodAutoscalerConfig) []corev1.EnvVar { 393 | envVars := []corev1.EnvVar{} 394 | for _, config := range configs { 395 | envVars = append(envVars, corev1.EnvVar{ 396 | Name: config.Name, 397 | Value: config.Value, 398 | }) 399 | } 400 | return envVars 401 | } 402 | 403 | // SetupWithManager sets up the CustomPodAutoscaler controller, setting up watches with the 404 | // manager provided 405 | func (r *CustomPodAutoscalerReconciler) SetupWithManager(mgr ctrl.Manager) error { 406 | return ctrl.NewControllerManagedBy(mgr). 407 | For(&custompodautoscalercomv1.CustomPodAutoscaler{}). 408 | WithEventFilter(PrimaryPred). 409 | Owns(&corev1.Pod{}, builder.WithPredicates(SecondaryPred)). 410 | Owns(&corev1.ServiceAccount{}, builder.WithPredicates(SecondaryPred)). 411 | Owns(&rbacv1.Role{}, builder.WithPredicates(SecondaryPred)). 412 | Owns(&rbacv1.RoleBinding{}, builder.WithPredicates(SecondaryPred)). 413 | Complete(r) 414 | } 415 | 416 | // SetupScalingClient sets up a client for the CPA reconciler to use for manually 417 | // setting the replicas count of a scale target pod while the autoscaler is paused. 418 | // Functionality is based on the setup for a regular CPA autoscaler in main() 419 | func SetupScalingClient() (k8sscale.ScalesGetter, error) { 420 | 421 | // InClusterConfig returns a config object which uses the service account 422 | // kubernetes gives to pods. It's intended for clients that expect to be 423 | // running inside a pod running on kubernetes. It will return ErrNotInCluster 424 | // if called from a process not running in a kubernetes environment. 425 | // https://github.com/kubernetes/client-go/blob/master/rest/config.go 426 | clusterConfig, err := rest.InClusterConfig() 427 | if err != nil { 428 | return nil, err 429 | } 430 | 431 | // NewForConfig creates a new ScalesGetter which resolves kinds 432 | // to resources using the given RESTMapper, and API paths using 433 | // the given dynamic.APIPathResolverFunc. 434 | // https://github.com/kubernetes/client-go/blob/master/scale/client.go 435 | clientset, err := kubernetes.NewForConfig(clusterConfig) 436 | if err != nil { 437 | return nil, err 438 | } 439 | 440 | // GetAPIGroupResources uses the provided discovery client to gather 441 | // discovery information and populate a slice of APIGroupResources 442 | // APIGroupResources{Group metav1.APIGroup, VersionedResources map[string][]metav1.APIResource} 443 | // https://github.com/kubernetes/client-go/blob/master/restmapper/discovery.go 444 | groupResources, err := restmapper.GetAPIGroupResources(clientset.Discovery()) 445 | if err != nil { 446 | return nil, err 447 | } 448 | 449 | // Set up a client for scaling 450 | // https://github.com/kubernetes/client-go/blob/master/scale/client.go 451 | scaleClient := k8sscale.New( 452 | clientset.RESTClient(), 453 | restmapper.NewDiscoveryRESTMapper(groupResources), 454 | dynamic.LegacyAPIPathResolverFunc, 455 | k8sscale.NewDiscoveryScaleKindResolver( 456 | clientset.Discovery(), 457 | ), 458 | ) 459 | 460 | return scaleClient, err 461 | } 462 | -------------------------------------------------------------------------------- /controllers/custompodautoscaler_controller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Custom Pod Autoscaler Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers_test 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "testing" 23 | "time" 24 | 25 | "sigs.k8s.io/controller-runtime/pkg/event" 26 | 27 | "github.com/go-logr/logr" 28 | "github.com/google/go-cmp/cmp" 29 | custompodautoscalercomv1 "github.com/jthomperoo/custom-pod-autoscaler-operator/api/v1" 30 | "github.com/jthomperoo/custom-pod-autoscaler-operator/controllers" 31 | autoscalingv1 "k8s.io/api/autoscaling/v1" 32 | corev1 "k8s.io/api/core/v1" 33 | rbacv1 "k8s.io/api/rbac/v1" 34 | "k8s.io/apimachinery/pkg/api/meta" 35 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 | "k8s.io/apimachinery/pkg/runtime" 37 | "k8s.io/apimachinery/pkg/runtime/schema" 38 | "k8s.io/apimachinery/pkg/types" 39 | k8sscale "k8s.io/client-go/scale" 40 | "sigs.k8s.io/controller-runtime/pkg/client" 41 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 42 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 43 | 44 | scaleFake "k8s.io/client-go/scale/fake" 45 | k8stesting "k8s.io/client-go/testing" 46 | ) 47 | 48 | func boolPtr(val bool) *bool { 49 | return &val 50 | } 51 | 52 | func TestPrimaryPredicate(t *testing.T) { 53 | result := controllers.PrimaryPred.Create(event.CreateEvent{}) 54 | if !cmp.Equal(result, true) { 55 | t.Errorf("Boolean mismatch (-want +got):\n%s", cmp.Diff(result, true)) 56 | return 57 | } 58 | result = controllers.PrimaryPred.Delete(event.DeleteEvent{}) 59 | if !cmp.Equal(result, true) { 60 | t.Errorf("Boolean mismatch (-want +got):\n%s", cmp.Diff(result, true)) 61 | return 62 | } 63 | result = controllers.PrimaryPred.Update(event.UpdateEvent{}) 64 | if !cmp.Equal(result, true) { 65 | t.Errorf("Boolean mismatch (-want +got):\n%s", cmp.Diff(result, true)) 66 | return 67 | } 68 | result = controllers.PrimaryPred.Generic(event.GenericEvent{}) 69 | if !cmp.Equal(result, false) { 70 | t.Errorf("Boolean mismatch (-want +got):\n%s", cmp.Diff(result, false)) 71 | return 72 | } 73 | } 74 | 75 | func TestSecondaryPredicate(t *testing.T) { 76 | result := controllers.SecondaryPred.Create(event.CreateEvent{}) 77 | if !cmp.Equal(result, false) { 78 | t.Errorf("Boolean mismatch (-want +got):\n%s", cmp.Diff(result, false)) 79 | return 80 | } 81 | result = controllers.SecondaryPred.Delete(event.DeleteEvent{}) 82 | if !cmp.Equal(result, true) { 83 | t.Errorf("Boolean mismatch (-want +got):\n%s", cmp.Diff(result, true)) 84 | return 85 | } 86 | result = controllers.SecondaryPred.Update(event.UpdateEvent{}) 87 | if !cmp.Equal(result, false) { 88 | t.Errorf("Boolean mismatch (-want +got):\n%s", cmp.Diff(result, false)) 89 | return 90 | } 91 | result = controllers.SecondaryPred.Generic(event.GenericEvent{}) 92 | if !cmp.Equal(result, false) { 93 | t.Errorf("Boolean mismatch (-want +got):\n%s", cmp.Diff(result, false)) 94 | return 95 | } 96 | } 97 | 98 | type fakek8sReconciler struct { 99 | reconcile func( 100 | reqLogger logr.Logger, 101 | instance *custompodautoscalercomv1.CustomPodAutoscaler, 102 | obj metav1.Object, 103 | shouldProvision bool, 104 | updatable bool, 105 | kind string, 106 | ) (reconcile.Result, error) 107 | 108 | podCleanup func(reqLogger logr.Logger, instance *custompodautoscalercomv1.CustomPodAutoscaler) error 109 | } 110 | 111 | func (f *fakek8sReconciler) Reconcile( 112 | reqLogger logr.Logger, 113 | instance *custompodautoscalercomv1.CustomPodAutoscaler, 114 | obj metav1.Object, 115 | shouldProvision bool, 116 | updatable bool, 117 | kind string, 118 | ) (reconcile.Result, error) { 119 | return f.reconcile(reqLogger, instance, obj, shouldProvision, updatable, kind) 120 | } 121 | 122 | func (f *fakek8sReconciler) PodCleanup(reqLogger logr.Logger, instance *custompodautoscalercomv1.CustomPodAutoscaler) error { 123 | return f.podCleanup(reqLogger, instance) 124 | } 125 | 126 | type fakeClient struct { 127 | get func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error 128 | list func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error 129 | create func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error 130 | delete func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error 131 | update func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error 132 | patch func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error 133 | deleteAllOf func(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error 134 | status func() client.StatusWriter 135 | scheme func() *runtime.Scheme 136 | restMapper func() meta.RESTMapper 137 | groupVersionKindFor func(obj runtime.Object) (schema.GroupVersionKind, error) 138 | isObjectNamespaced func(obj runtime.Object) (bool, error) 139 | subResource func(subResource string) client.SubResourceClient 140 | } 141 | 142 | func (f *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 143 | return f.get(ctx, key, obj) 144 | } 145 | 146 | func (f *fakeClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { 147 | return f.list(ctx, list, opts...) 148 | } 149 | 150 | func (f *fakeClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { 151 | return f.create(ctx, obj, opts...) 152 | } 153 | 154 | func (f *fakeClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { 155 | return f.delete(ctx, obj, opts...) 156 | } 157 | 158 | func (f *fakeClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 159 | return f.update(ctx, obj, opts...) 160 | } 161 | 162 | func (f *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { 163 | return f.patch(ctx, obj, patch, opts...) 164 | } 165 | 166 | func (f *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { 167 | return f.deleteAllOf(ctx, obj, opts...) 168 | } 169 | 170 | func (f *fakeClient) Status() client.StatusWriter { 171 | return f.status() 172 | } 173 | 174 | func (f *fakeClient) Scheme() *runtime.Scheme { 175 | return f.scheme() 176 | } 177 | 178 | func (f *fakeClient) RESTMapper() meta.RESTMapper { 179 | return f.restMapper() 180 | } 181 | 182 | func (f *fakeClient) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { 183 | return f.groupVersionKindFor(obj) 184 | } 185 | 186 | func (f *fakeClient) IsObjectNamespaced(obj runtime.Object) (bool, error) { 187 | return f.isObjectNamespaced(obj) 188 | } 189 | 190 | func (f *fakeClient) SubResource(subResource string) client.SubResourceClient { 191 | return f.subResource(subResource) 192 | } 193 | 194 | func TestReconcile(t *testing.T) { 195 | equateErrorMessage := cmp.Comparer(func(x, y error) bool { 196 | if x == nil || y == nil { 197 | return x == nil && y == nil 198 | } 199 | return x.Error() == y.Error() 200 | }) 201 | 202 | var tests = []struct { 203 | description string 204 | expected reconcile.Result 205 | expectedErr error 206 | client client.Client 207 | request reconcile.Request 208 | k8sreconciler controllers.K8sReconciler 209 | scalingClient k8sscale.ScalesGetter 210 | }{ 211 | { 212 | "No matching CPA", 213 | reconcile.Result{}, 214 | nil, 215 | fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 216 | s := runtime.NewScheme() 217 | s.AddKnownTypes(custompodautoscalercomv1.GroupVersion, &custompodautoscalercomv1.CustomPodAutoscaler{ 218 | ObjectMeta: metav1.ObjectMeta{ 219 | Name: "test", 220 | Namespace: "test-namespace", 221 | }, 222 | }) 223 | return s 224 | }()).Build(), 225 | reconcile.Request{ 226 | NamespacedName: types.NamespacedName{ 227 | Name: "test", 228 | Namespace: "test-namespace", 229 | }, 230 | }, 231 | nil, 232 | nil, 233 | }, 234 | { 235 | "Error on getting CPA", 236 | reconcile.Result{}, 237 | errors.New("Error getting CPA"), 238 | func() *fakeClient { 239 | fclient := &fakeClient{} 240 | fclient.get = func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 241 | return errors.New("Error getting CPA") 242 | } 243 | return fclient 244 | }(), 245 | reconcile.Request{ 246 | NamespacedName: types.NamespacedName{ 247 | Name: "test", 248 | Namespace: "test-namespace", 249 | }, 250 | }, 251 | nil, 252 | nil, 253 | }, 254 | { 255 | "Fail to reconcile service account", 256 | reconcile.Result{}, 257 | errors.New("Error reconciling service account"), 258 | fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 259 | s := runtime.NewScheme() 260 | s.AddKnownTypes(custompodautoscalercomv1.GroupVersion, &custompodautoscalercomv1.CustomPodAutoscaler{ 261 | ObjectMeta: metav1.ObjectMeta{ 262 | Name: "test", 263 | Namespace: "test-namespace", 264 | }, 265 | }) 266 | return s 267 | }()).WithRuntimeObjects( 268 | &custompodautoscalercomv1.CustomPodAutoscaler{ 269 | ObjectMeta: metav1.ObjectMeta{ 270 | Name: "test", 271 | Namespace: "test-namespace", 272 | }, 273 | }, 274 | ).Build(), 275 | reconcile.Request{ 276 | NamespacedName: types.NamespacedName{ 277 | Name: "test", 278 | Namespace: "test-namespace", 279 | }, 280 | }, 281 | func() *fakek8sReconciler { 282 | reconciler := &fakek8sReconciler{} 283 | reconciler.reconcile = func( 284 | reqLogger logr.Logger, 285 | instance *custompodautoscalercomv1.CustomPodAutoscaler, 286 | obj metav1.Object, 287 | shouldProvision bool, 288 | updatable bool, 289 | kind string, 290 | ) (reconcile.Result, error) { 291 | _, ok := obj.(*corev1.ServiceAccount) 292 | if ok { 293 | return reconcile.Result{}, errors.New("Error reconciling service account") 294 | } 295 | return reconcile.Result{}, nil 296 | } 297 | return reconciler 298 | }(), 299 | nil, 300 | }, 301 | { 302 | "Fail to reconcile role", 303 | reconcile.Result{}, 304 | errors.New("Error reconciling role"), 305 | fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 306 | s := runtime.NewScheme() 307 | s.AddKnownTypes(custompodautoscalercomv1.GroupVersion, &custompodautoscalercomv1.CustomPodAutoscaler{ 308 | ObjectMeta: metav1.ObjectMeta{ 309 | Name: "test", 310 | Namespace: "test-namespace", 311 | }, 312 | }) 313 | return s 314 | }()).WithRuntimeObjects( 315 | &custompodautoscalercomv1.CustomPodAutoscaler{ 316 | ObjectMeta: metav1.ObjectMeta{ 317 | Name: "test", 318 | Namespace: "test-namespace", 319 | }, 320 | }, 321 | ).Build(), 322 | reconcile.Request{ 323 | NamespacedName: types.NamespacedName{ 324 | Name: "test", 325 | Namespace: "test-namespace", 326 | }, 327 | }, 328 | func() *fakek8sReconciler { 329 | reconciler := &fakek8sReconciler{} 330 | reconciler.reconcile = func( 331 | reqLogger logr.Logger, 332 | instance *custompodautoscalercomv1.CustomPodAutoscaler, 333 | obj metav1.Object, 334 | shouldProvision bool, 335 | updatable bool, 336 | kind string, 337 | ) (reconcile.Result, error) { 338 | _, ok := obj.(*rbacv1.Role) 339 | if ok { 340 | return reconcile.Result{}, errors.New("Error reconciling role") 341 | } 342 | return reconcile.Result{}, nil 343 | } 344 | return reconciler 345 | }(), 346 | nil, 347 | }, 348 | { 349 | "Fail to reconcile role binding", 350 | reconcile.Result{}, 351 | errors.New("Error reconciling rolebinding"), 352 | fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 353 | s := runtime.NewScheme() 354 | s.AddKnownTypes(custompodautoscalercomv1.GroupVersion, &custompodautoscalercomv1.CustomPodAutoscaler{ 355 | ObjectMeta: metav1.ObjectMeta{ 356 | Name: "test", 357 | Namespace: "test-namespace", 358 | }, 359 | }) 360 | return s 361 | }()).WithRuntimeObjects( 362 | &custompodautoscalercomv1.CustomPodAutoscaler{ 363 | ObjectMeta: metav1.ObjectMeta{ 364 | Name: "test", 365 | Namespace: "test-namespace", 366 | }, 367 | }, 368 | ).Build(), 369 | reconcile.Request{ 370 | NamespacedName: types.NamespacedName{ 371 | Name: "test", 372 | Namespace: "test-namespace", 373 | }, 374 | }, 375 | func() *fakek8sReconciler { 376 | reconciler := &fakek8sReconciler{} 377 | reconciler.reconcile = func( 378 | reqLogger logr.Logger, 379 | instance *custompodautoscalercomv1.CustomPodAutoscaler, 380 | obj metav1.Object, 381 | shouldProvision bool, 382 | updatable bool, 383 | kind string, 384 | ) (reconcile.Result, error) { 385 | _, ok := obj.(*rbacv1.RoleBinding) 386 | if ok { 387 | return reconcile.Result{}, errors.New("Error reconciling rolebinding") 388 | } 389 | return reconcile.Result{}, nil 390 | } 391 | return reconciler 392 | }(), 393 | nil, 394 | }, 395 | { 396 | "Fail to reconcile pod", 397 | reconcile.Result{}, 398 | errors.New("Error reconciling pod"), 399 | fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 400 | s := runtime.NewScheme() 401 | s.AddKnownTypes(custompodautoscalercomv1.GroupVersion, &custompodautoscalercomv1.CustomPodAutoscaler{}) 402 | return s 403 | }()).WithRuntimeObjects( 404 | &custompodautoscalercomv1.CustomPodAutoscaler{ 405 | Spec: custompodautoscalercomv1.CustomPodAutoscalerSpec{ 406 | Template: custompodautoscalercomv1.PodTemplateSpec{}, 407 | }, 408 | ObjectMeta: metav1.ObjectMeta{ 409 | Name: "test", 410 | Namespace: "test-namespace", 411 | }, 412 | }, 413 | ).Build(), 414 | reconcile.Request{ 415 | NamespacedName: types.NamespacedName{ 416 | Name: "test", 417 | Namespace: "test-namespace", 418 | }, 419 | }, 420 | func() *fakek8sReconciler { 421 | reconciler := &fakek8sReconciler{} 422 | reconciler.reconcile = func( 423 | reqLogger logr.Logger, 424 | instance *custompodautoscalercomv1.CustomPodAutoscaler, 425 | obj metav1.Object, 426 | shouldProvision bool, 427 | updatable bool, 428 | kind string, 429 | ) (reconcile.Result, error) { 430 | _, ok := obj.(*corev1.Pod) 431 | if ok { 432 | return reconcile.Result{}, errors.New("Error reconciling pod") 433 | } 434 | return reconcile.Result{}, nil 435 | } 436 | return reconciler 437 | }(), 438 | nil, 439 | }, 440 | { 441 | "Fail to clean up orphaned pods", 442 | reconcile.Result{}, 443 | errors.New("Error cleaning up pods"), 444 | fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 445 | s := runtime.NewScheme() 446 | s.AddKnownTypes(custompodautoscalercomv1.GroupVersion, &custompodautoscalercomv1.CustomPodAutoscaler{ 447 | ObjectMeta: metav1.ObjectMeta{ 448 | Name: "test", 449 | Namespace: "test-namespace", 450 | }, 451 | }) 452 | return s 453 | }()).WithRuntimeObjects( 454 | &custompodautoscalercomv1.CustomPodAutoscaler{ 455 | Spec: custompodautoscalercomv1.CustomPodAutoscalerSpec{ 456 | Template: custompodautoscalercomv1.PodTemplateSpec{ 457 | Spec: custompodautoscalercomv1.PodSpec{ 458 | Containers: []corev1.Container{ 459 | { 460 | Name: "test container", 461 | }, 462 | }, 463 | }, 464 | }, 465 | }, 466 | ObjectMeta: metav1.ObjectMeta{ 467 | Name: "test", 468 | Namespace: "test-namespace", 469 | }, 470 | }, 471 | ).Build(), 472 | reconcile.Request{ 473 | NamespacedName: types.NamespacedName{ 474 | Name: "test", 475 | Namespace: "test-namespace", 476 | }, 477 | }, 478 | func() *fakek8sReconciler { 479 | reconciler := &fakek8sReconciler{} 480 | reconciler.reconcile = func( 481 | reqLogger logr.Logger, 482 | instance *custompodautoscalercomv1.CustomPodAutoscaler, 483 | obj metav1.Object, 484 | shouldProvision bool, 485 | updatable bool, 486 | kind string, 487 | ) (reconcile.Result, error) { 488 | pod, ok := obj.(*corev1.Pod) 489 | if ok { 490 | // Default env vars 491 | expectedEnvVars := []corev1.EnvVar{ 492 | { 493 | Name: "scaleTargetRef", 494 | Value: `{"kind":"","name":""}`, 495 | }, 496 | { 497 | Name: "namespace", 498 | Value: "test-namespace", 499 | }, 500 | } 501 | 502 | if !cmp.Equal(expectedEnvVars, pod.Spec.Containers[0].Env) { 503 | t.Errorf("Env vars mismatch (-want +got):\n%s", 504 | cmp.Diff(expectedEnvVars, pod.Spec.Containers[0].Env)) 505 | return reconcile.Result{}, nil 506 | } 507 | return reconcile.Result{}, nil 508 | } 509 | return reconcile.Result{}, nil 510 | } 511 | reconciler.podCleanup = func(reqLogger logr.Logger, instance *custompodautoscalercomv1.CustomPodAutoscaler) error { 512 | return errors.New("Error cleaning up pods") 513 | } 514 | return reconciler 515 | }(), 516 | nil, 517 | }, 518 | { 519 | "Successfully reconcile with no env vars", 520 | reconcile.Result{}, 521 | nil, 522 | fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 523 | s := runtime.NewScheme() 524 | s.AddKnownTypes(custompodautoscalercomv1.GroupVersion, &custompodautoscalercomv1.CustomPodAutoscaler{ 525 | ObjectMeta: metav1.ObjectMeta{ 526 | Name: "test", 527 | Namespace: "test-namespace", 528 | }, 529 | }) 530 | return s 531 | }()).WithRuntimeObjects( 532 | &custompodautoscalercomv1.CustomPodAutoscaler{ 533 | Spec: custompodautoscalercomv1.CustomPodAutoscalerSpec{ 534 | Template: custompodautoscalercomv1.PodTemplateSpec{ 535 | Spec: custompodautoscalercomv1.PodSpec{ 536 | Containers: []corev1.Container{ 537 | { 538 | Name: "test container", 539 | }, 540 | }, 541 | }, 542 | }, 543 | }, 544 | ObjectMeta: metav1.ObjectMeta{ 545 | Name: "test", 546 | Namespace: "test-namespace", 547 | }, 548 | }, 549 | ).Build(), 550 | reconcile.Request{ 551 | NamespacedName: types.NamespacedName{ 552 | Name: "test", 553 | Namespace: "test-namespace", 554 | }, 555 | }, 556 | func() *fakek8sReconciler { 557 | reconciler := &fakek8sReconciler{} 558 | reconciler.reconcile = func( 559 | reqLogger logr.Logger, 560 | instance *custompodautoscalercomv1.CustomPodAutoscaler, 561 | obj metav1.Object, 562 | shouldProvision bool, 563 | updatable bool, 564 | kind string, 565 | ) (reconcile.Result, error) { 566 | pod, ok := obj.(*corev1.Pod) 567 | if ok { 568 | // Default env vars 569 | expectedEnvVars := []corev1.EnvVar{ 570 | { 571 | Name: "scaleTargetRef", 572 | Value: `{"kind":"","name":""}`, 573 | }, 574 | { 575 | Name: "namespace", 576 | Value: "test-namespace", 577 | }, 578 | } 579 | 580 | if !cmp.Equal(expectedEnvVars, pod.Spec.Containers[0].Env) { 581 | t.Errorf("Env vars mismatch (-want +got):\n%s", 582 | cmp.Diff(expectedEnvVars, pod.Spec.Containers[0].Env)) 583 | return reconcile.Result{}, nil 584 | } 585 | return reconcile.Result{}, nil 586 | } 587 | return reconcile.Result{}, nil 588 | } 589 | reconciler.podCleanup = func(reqLogger logr.Logger, instance *custompodautoscalercomv1.CustomPodAutoscaler) error { 590 | return nil 591 | } 592 | return reconciler 593 | }(), 594 | nil, 595 | }, 596 | { 597 | "Successfully reconcile with env vars", 598 | reconcile.Result{}, 599 | nil, 600 | fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 601 | s := runtime.NewScheme() 602 | s.AddKnownTypes(custompodautoscalercomv1.GroupVersion, &custompodautoscalercomv1.CustomPodAutoscaler{ 603 | ObjectMeta: metav1.ObjectMeta{ 604 | Name: "test", 605 | Namespace: "test-namespace", 606 | }, 607 | }) 608 | return s 609 | }()).WithRuntimeObjects( 610 | &custompodautoscalercomv1.CustomPodAutoscaler{ 611 | ObjectMeta: metav1.ObjectMeta{ 612 | Name: "test", 613 | Namespace: "test-namespace", 614 | }, 615 | Spec: custompodautoscalercomv1.CustomPodAutoscalerSpec{ 616 | Template: custompodautoscalercomv1.PodTemplateSpec{ 617 | Spec: custompodautoscalercomv1.PodSpec{ 618 | Containers: []corev1.Container{ 619 | { 620 | Name: "test container", 621 | }, 622 | }, 623 | }, 624 | }, 625 | Config: []custompodautoscalercomv1.CustomPodAutoscalerConfig{ 626 | { 627 | Name: "first env var", 628 | Value: "first env var value", 629 | }, 630 | { 631 | Name: "second env var", 632 | Value: "second env var value", 633 | }, 634 | { 635 | Name: "third env var", 636 | Value: "third env var value", 637 | }, 638 | }, 639 | }, 640 | }, 641 | ).Build(), 642 | reconcile.Request{ 643 | NamespacedName: types.NamespacedName{ 644 | Name: "test", 645 | Namespace: "test-namespace", 646 | }, 647 | }, 648 | func() *fakek8sReconciler { 649 | reconciler := &fakek8sReconciler{} 650 | reconciler.reconcile = func( 651 | reqLogger logr.Logger, 652 | instance *custompodautoscalercomv1.CustomPodAutoscaler, 653 | obj metav1.Object, 654 | shouldProvision bool, 655 | updatable bool, 656 | kind string, 657 | ) (reconcile.Result, error) { 658 | pod, ok := obj.(*corev1.Pod) 659 | if ok { 660 | expectedEnvVars := []corev1.EnvVar{ 661 | { 662 | Name: "scaleTargetRef", 663 | Value: `{"kind":"","name":""}`, 664 | }, 665 | { 666 | Name: "namespace", 667 | Value: "test-namespace", 668 | }, 669 | { 670 | Name: "first env var", 671 | Value: "first env var value", 672 | }, 673 | { 674 | Name: "second env var", 675 | Value: "second env var value", 676 | }, 677 | { 678 | Name: "third env var", 679 | Value: "third env var value", 680 | }, 681 | } 682 | 683 | if !cmp.Equal(expectedEnvVars, pod.Spec.Containers[0].Env) { 684 | t.Errorf("Env vars mismatch (-want +got):\n%s", 685 | cmp.Diff(expectedEnvVars, pod.Spec.Containers[0].Env)) 686 | return reconcile.Result{}, nil 687 | } 688 | return reconcile.Result{}, nil 689 | } 690 | return reconcile.Result{}, nil 691 | } 692 | reconciler.podCleanup = func(reqLogger logr.Logger, instance *custompodautoscalercomv1.CustomPodAutoscaler) error { 693 | return nil 694 | } 695 | return reconciler 696 | }(), 697 | nil, 698 | }, 699 | { 700 | "Successfully reconcile with labels set in the container", 701 | reconcile.Result{}, 702 | nil, 703 | fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 704 | s := runtime.NewScheme() 705 | s.AddKnownTypes(custompodautoscalercomv1.GroupVersion, &custompodautoscalercomv1.CustomPodAutoscaler{ 706 | ObjectMeta: metav1.ObjectMeta{ 707 | Name: "test", 708 | Namespace: "test-namespace", 709 | }, 710 | }) 711 | return s 712 | }()).WithRuntimeObjects( 713 | &custompodautoscalercomv1.CustomPodAutoscaler{ 714 | ObjectMeta: metav1.ObjectMeta{ 715 | Name: "test", 716 | Namespace: "test-namespace", 717 | }, 718 | Spec: custompodautoscalercomv1.CustomPodAutoscalerSpec{ 719 | Template: custompodautoscalercomv1.PodTemplateSpec{ 720 | Spec: custompodautoscalercomv1.PodSpec{ 721 | Containers: []corev1.Container{ 722 | { 723 | Name: "test container", 724 | }, 725 | }, 726 | }, 727 | ObjectMeta: custompodautoscalercomv1.PodMeta{ 728 | Labels: map[string]string{ 729 | "test-label": "test", 730 | }, 731 | }, 732 | }, 733 | }, 734 | }, 735 | ).Build(), 736 | reconcile.Request{ 737 | NamespacedName: types.NamespacedName{ 738 | Name: "test", 739 | Namespace: "test-namespace", 740 | }, 741 | }, 742 | func() *fakek8sReconciler { 743 | reconciler := &fakek8sReconciler{} 744 | reconciler.reconcile = func( 745 | reqLogger logr.Logger, 746 | instance *custompodautoscalercomv1.CustomPodAutoscaler, 747 | obj metav1.Object, 748 | shouldProvision bool, 749 | updatable bool, 750 | kind string, 751 | ) (reconcile.Result, error) { 752 | pod, ok := obj.(*corev1.Pod) 753 | if ok { 754 | expectedEnvVars := []corev1.EnvVar{ 755 | { 756 | Name: "scaleTargetRef", 757 | Value: `{"kind":"","name":""}`, 758 | }, 759 | { 760 | Name: "namespace", 761 | Value: "test-namespace", 762 | }, 763 | } 764 | 765 | if !cmp.Equal(expectedEnvVars, pod.Spec.Containers[0].Env) { 766 | t.Errorf("Env vars mismatch (-want +got):\n%s", 767 | cmp.Diff(expectedEnvVars, pod.Spec.Containers[0].Env)) 768 | return reconcile.Result{}, nil 769 | } 770 | return reconcile.Result{}, nil 771 | } 772 | return reconcile.Result{}, nil 773 | } 774 | reconciler.podCleanup = func(reqLogger logr.Logger, instance *custompodautoscalercomv1.CustomPodAutoscaler) error { 775 | return nil 776 | } 777 | return reconciler 778 | }(), 779 | nil, 780 | }, 781 | { 782 | "Successfully reconcile with env vars set in pod spec and no config env vars", 783 | reconcile.Result{}, 784 | nil, 785 | fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 786 | s := runtime.NewScheme() 787 | s.AddKnownTypes(custompodautoscalercomv1.GroupVersion, &custompodautoscalercomv1.CustomPodAutoscaler{ 788 | ObjectMeta: metav1.ObjectMeta{ 789 | Name: "test", 790 | Namespace: "test-namespace", 791 | }, 792 | }) 793 | return s 794 | }()).WithRuntimeObjects( 795 | &custompodautoscalercomv1.CustomPodAutoscaler{ 796 | ObjectMeta: metav1.ObjectMeta{ 797 | Name: "test", 798 | Namespace: "test-namespace", 799 | }, 800 | Spec: custompodautoscalercomv1.CustomPodAutoscalerSpec{ 801 | Template: custompodautoscalercomv1.PodTemplateSpec{ 802 | Spec: custompodautoscalercomv1.PodSpec{ 803 | Containers: []corev1.Container{ 804 | { 805 | Name: "test container", 806 | Env: []corev1.EnvVar{ 807 | { 808 | Name: "test container env name", 809 | Value: "test container env value", 810 | }, 811 | }, 812 | }, 813 | }, 814 | }, 815 | }, 816 | }, 817 | }, 818 | ).Build(), 819 | reconcile.Request{ 820 | NamespacedName: types.NamespacedName{ 821 | Name: "test", 822 | Namespace: "test-namespace", 823 | }, 824 | }, 825 | func() *fakek8sReconciler { 826 | reconciler := &fakek8sReconciler{} 827 | reconciler.reconcile = func( 828 | reqLogger logr.Logger, 829 | instance *custompodautoscalercomv1.CustomPodAutoscaler, 830 | obj metav1.Object, 831 | shouldProvision bool, 832 | updatable bool, 833 | kind string, 834 | ) (reconcile.Result, error) { 835 | pod, ok := obj.(*corev1.Pod) 836 | if ok { 837 | expectedEnvVars := []corev1.EnvVar{ 838 | { 839 | Name: "test container env name", 840 | Value: "test container env value", 841 | }, 842 | { 843 | Name: "scaleTargetRef", 844 | Value: `{"kind":"","name":""}`, 845 | }, 846 | { 847 | Name: "namespace", 848 | Value: "test-namespace", 849 | }, 850 | } 851 | 852 | if !cmp.Equal(expectedEnvVars, pod.Spec.Containers[0].Env) { 853 | t.Errorf("Env vars mismatch (-want +got):\n%s", 854 | cmp.Diff(expectedEnvVars, pod.Spec.Containers[0].Env)) 855 | return reconcile.Result{}, nil 856 | } 857 | return reconcile.Result{}, nil 858 | } 859 | return reconcile.Result{}, nil 860 | } 861 | reconciler.podCleanup = func(reqLogger logr.Logger, instance *custompodautoscalercomv1.CustomPodAutoscaler) error { 862 | return nil 863 | } 864 | return reconciler 865 | }(), 866 | nil, 867 | }, 868 | { 869 | "Successfully reconcile while requesting a role with access to the metrics server", 870 | reconcile.Result{}, 871 | nil, 872 | fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 873 | s := runtime.NewScheme() 874 | s.AddKnownTypes(custompodautoscalercomv1.GroupVersion, &custompodautoscalercomv1.CustomPodAutoscaler{ 875 | ObjectMeta: metav1.ObjectMeta{ 876 | Name: "test", 877 | Namespace: "test-namespace", 878 | }, 879 | }) 880 | return s 881 | }()).WithRuntimeObjects( 882 | &custompodautoscalercomv1.CustomPodAutoscaler{ 883 | Spec: custompodautoscalercomv1.CustomPodAutoscalerSpec{ 884 | Template: custompodautoscalercomv1.PodTemplateSpec{ 885 | Spec: custompodautoscalercomv1.PodSpec{ 886 | Containers: []corev1.Container{ 887 | { 888 | Name: "test container", 889 | }, 890 | }, 891 | }, 892 | }, 893 | RoleRequiresMetricsServer: boolPtr(true), 894 | }, 895 | ObjectMeta: metav1.ObjectMeta{ 896 | Name: "test", 897 | Namespace: "test-namespace", 898 | }, 899 | }, 900 | ).Build(), 901 | reconcile.Request{ 902 | NamespacedName: types.NamespacedName{ 903 | Name: "test", 904 | Namespace: "test-namespace", 905 | }, 906 | }, 907 | func() *fakek8sReconciler { 908 | reconciler := &fakek8sReconciler{} 909 | reconciler.reconcile = func( 910 | reqLogger logr.Logger, 911 | instance *custompodautoscalercomv1.CustomPodAutoscaler, 912 | obj metav1.Object, 913 | shouldProvision bool, 914 | updatable bool, 915 | kind string, 916 | ) (reconcile.Result, error) { 917 | return reconcile.Result{}, nil 918 | } 919 | reconciler.podCleanup = func(reqLogger logr.Logger, instance *custompodautoscalercomv1.CustomPodAutoscaler) error { 920 | return nil 921 | } 922 | return reconciler 923 | }(), 924 | nil, 925 | }, 926 | { 927 | "Successfully reconcile while requesting a role with access to manage argo rollouts", 928 | reconcile.Result{}, 929 | nil, 930 | fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 931 | s := runtime.NewScheme() 932 | s.AddKnownTypes(custompodautoscalercomv1.GroupVersion, &custompodautoscalercomv1.CustomPodAutoscaler{ 933 | ObjectMeta: metav1.ObjectMeta{ 934 | Name: "test", 935 | Namespace: "test-namespace", 936 | }, 937 | }) 938 | return s 939 | }()).WithRuntimeObjects( 940 | &custompodautoscalercomv1.CustomPodAutoscaler{ 941 | Spec: custompodautoscalercomv1.CustomPodAutoscalerSpec{ 942 | Template: custompodautoscalercomv1.PodTemplateSpec{ 943 | Spec: custompodautoscalercomv1.PodSpec{ 944 | Containers: []corev1.Container{ 945 | { 946 | Name: "test container", 947 | }, 948 | }, 949 | }, 950 | }, 951 | RoleRequiresArgoRollouts: boolPtr(true), 952 | }, 953 | ObjectMeta: metav1.ObjectMeta{ 954 | Name: "test", 955 | Namespace: "test-namespace", 956 | }, 957 | }, 958 | ).Build(), 959 | reconcile.Request{ 960 | NamespacedName: types.NamespacedName{ 961 | Name: "test", 962 | Namespace: "test-namespace", 963 | }, 964 | }, 965 | func() *fakek8sReconciler { 966 | reconciler := &fakek8sReconciler{} 967 | reconciler.reconcile = func( 968 | reqLogger logr.Logger, 969 | instance *custompodautoscalercomv1.CustomPodAutoscaler, 970 | obj metav1.Object, 971 | shouldProvision bool, 972 | updatable bool, 973 | kind string, 974 | ) (reconcile.Result, error) { 975 | return reconcile.Result{}, nil 976 | } 977 | reconciler.podCleanup = func(reqLogger logr.Logger, instance *custompodautoscalercomv1.CustomPodAutoscaler) error { 978 | return nil 979 | } 980 | return reconciler 981 | }(), 982 | nil, 983 | }, 984 | { 985 | "Successfully reconcile when pause annotation present", 986 | reconcile.Result{}, 987 | nil, 988 | fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 989 | s := runtime.NewScheme() 990 | s.AddKnownTypes(custompodautoscalercomv1.GroupVersion, &custompodautoscalercomv1.CustomPodAutoscaler{ 991 | ObjectMeta: metav1.ObjectMeta{ 992 | Name: "test", 993 | Namespace: "test-namespace", 994 | }, 995 | }) 996 | return s 997 | }()).WithRuntimeObjects( 998 | &custompodautoscalercomv1.CustomPodAutoscaler{ 999 | ObjectMeta: metav1.ObjectMeta{ 1000 | Name: "test", 1001 | Namespace: "test-namespace", 1002 | Annotations: map[string]string{ 1003 | controllers.PausedReplicasAnnotation: "5", 1004 | }, 1005 | }, 1006 | Spec: custompodautoscalercomv1.CustomPodAutoscalerSpec{ 1007 | Template: custompodautoscalercomv1.PodTemplateSpec{ 1008 | Spec: custompodautoscalercomv1.PodSpec{ 1009 | Containers: []corev1.Container{ 1010 | { 1011 | Name: "test container", 1012 | }, 1013 | }, 1014 | }, 1015 | }, 1016 | }, 1017 | }, 1018 | ).Build(), 1019 | reconcile.Request{ 1020 | NamespacedName: types.NamespacedName{ 1021 | Name: "test", 1022 | Namespace: "test-namespace", 1023 | }, 1024 | }, 1025 | func() *fakek8sReconciler { 1026 | return &fakek8sReconciler{} 1027 | }(), 1028 | &scaleFake.FakeScaleClient{ 1029 | Fake: k8stesting.Fake{ 1030 | ReactionChain: []k8stesting.Reactor{ 1031 | &k8stesting.SimpleReactor{ 1032 | Resource: "*", 1033 | Verb: "get", 1034 | Reaction: func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { 1035 | return true, &autoscalingv1.Scale{}, nil 1036 | }, 1037 | }, 1038 | &k8stesting.SimpleReactor{ 1039 | Resource: "*", 1040 | Verb: "update", 1041 | Reaction: func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { 1042 | return true, &autoscalingv1.Scale{}, nil 1043 | }, 1044 | }, 1045 | }, 1046 | }, 1047 | }, 1048 | }, 1049 | { 1050 | "Fail reconcile when scale Get API call fails", 1051 | reconcile.Result{}, 1052 | errors.New(`Failed Get API call`), 1053 | fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 1054 | s := runtime.NewScheme() 1055 | s.AddKnownTypes(custompodautoscalercomv1.GroupVersion, 1056 | &custompodautoscalercomv1.CustomPodAutoscaler{ 1057 | ObjectMeta: metav1.ObjectMeta{ 1058 | Name: "test", 1059 | Namespace: "test-namespace", 1060 | }, 1061 | }) 1062 | return s 1063 | }()).WithRuntimeObjects( 1064 | &custompodautoscalercomv1.CustomPodAutoscaler{ 1065 | ObjectMeta: metav1.ObjectMeta{ 1066 | Name: "test", 1067 | Namespace: "test-namespace", 1068 | Annotations: map[string]string{ 1069 | controllers.PausedReplicasAnnotation: "5", 1070 | }, 1071 | }, 1072 | Spec: custompodautoscalercomv1.CustomPodAutoscalerSpec{ 1073 | Template: custompodautoscalercomv1.PodTemplateSpec{ 1074 | Spec: custompodautoscalercomv1.PodSpec{ 1075 | Containers: []corev1.Container{ 1076 | { 1077 | Name: "test container", 1078 | }, 1079 | }, 1080 | }, 1081 | }, 1082 | }, 1083 | }, 1084 | ).Build(), 1085 | reconcile.Request{ 1086 | NamespacedName: types.NamespacedName{ 1087 | Name: "test", 1088 | Namespace: "test-namespace", 1089 | }, 1090 | }, 1091 | func() *fakek8sReconciler { 1092 | return &fakek8sReconciler{} 1093 | }(), 1094 | &scaleFake.FakeScaleClient{ 1095 | Fake: k8stesting.Fake{ 1096 | ReactionChain: []k8stesting.Reactor{ 1097 | &k8stesting.SimpleReactor{ 1098 | Resource: "*", 1099 | Verb: "get", 1100 | Reaction: func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { 1101 | return true, nil, errors.New(`Failed Get API call`) 1102 | }, 1103 | }, 1104 | }, 1105 | }, 1106 | }, 1107 | }, 1108 | { 1109 | "Fail reconcile when scale Update API call fails", 1110 | reconcile.Result{}, 1111 | errors.New(`Failed Update API call`), 1112 | fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 1113 | s := runtime.NewScheme() 1114 | s.AddKnownTypes(custompodautoscalercomv1.GroupVersion, 1115 | &custompodautoscalercomv1.CustomPodAutoscaler{ 1116 | ObjectMeta: metav1.ObjectMeta{ 1117 | Name: "test", 1118 | Namespace: "test-namespace", 1119 | }, 1120 | }) 1121 | return s 1122 | }()).WithRuntimeObjects( 1123 | &custompodautoscalercomv1.CustomPodAutoscaler{ 1124 | ObjectMeta: metav1.ObjectMeta{ 1125 | Name: "test", 1126 | Namespace: "test-namespace", 1127 | Annotations: map[string]string{ 1128 | controllers.PausedReplicasAnnotation: "5", 1129 | }, 1130 | }, 1131 | Spec: custompodautoscalercomv1.CustomPodAutoscalerSpec{ 1132 | Template: custompodautoscalercomv1.PodTemplateSpec{ 1133 | Spec: custompodautoscalercomv1.PodSpec{ 1134 | Containers: []corev1.Container{ 1135 | { 1136 | Name: "test container", 1137 | }, 1138 | }, 1139 | }, 1140 | }, 1141 | }, 1142 | }, 1143 | ).Build(), 1144 | reconcile.Request{ 1145 | NamespacedName: types.NamespacedName{ 1146 | Name: "test", 1147 | Namespace: "test-namespace", 1148 | }, 1149 | }, 1150 | func() *fakek8sReconciler { 1151 | return &fakek8sReconciler{} 1152 | }(), 1153 | &scaleFake.FakeScaleClient{ 1154 | Fake: k8stesting.Fake{ 1155 | ReactionChain: []k8stesting.Reactor{ 1156 | &k8stesting.SimpleReactor{ 1157 | Resource: "*", 1158 | Verb: "update", 1159 | Reaction: func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { 1160 | return true, nil, errors.New(`Failed Update API call`) 1161 | }, 1162 | }, 1163 | }, 1164 | }, 1165 | }, 1166 | }, 1167 | { 1168 | "Successfully reconcile when marked for deletion as part of foreground cascade delete", 1169 | reconcile.Result{}, 1170 | nil, 1171 | func() *fakeClient { 1172 | fclient := &fakeClient{} 1173 | fclient.get = func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 1174 | cpa, _ := obj.(*custompodautoscalercomv1.CustomPodAutoscaler) 1175 | cpa.ObjectMeta = metav1.ObjectMeta{ 1176 | Name: "test", 1177 | Namespace: "test-namespace", 1178 | DeletionTimestamp: &metav1.Time{ 1179 | Time: time.Time{}, 1180 | }, 1181 | Finalizers: []string{ 1182 | "foregroundDeletion", 1183 | }, 1184 | } 1185 | return nil 1186 | } 1187 | return fclient 1188 | }(), 1189 | reconcile.Request{ 1190 | NamespacedName: types.NamespacedName{ 1191 | Name: "test", 1192 | Namespace: "test-namespace", 1193 | }, 1194 | }, 1195 | func() *fakek8sReconciler { 1196 | return &fakek8sReconciler{} 1197 | }(), 1198 | &scaleFake.FakeScaleClient{ 1199 | Fake: k8stesting.Fake{ 1200 | ReactionChain: []k8stesting.Reactor{ 1201 | &k8stesting.SimpleReactor{ 1202 | Resource: "*", 1203 | Verb: "get", 1204 | Reaction: func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { 1205 | return true, &autoscalingv1.Scale{}, nil 1206 | }, 1207 | }, 1208 | &k8stesting.SimpleReactor{ 1209 | Resource: "*", 1210 | Verb: "update", 1211 | Reaction: func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { 1212 | return true, &autoscalingv1.Scale{}, nil 1213 | }, 1214 | }, 1215 | }, 1216 | }, 1217 | }, 1218 | }, 1219 | } 1220 | for _, test := range tests { 1221 | t.Run(test.description, func(t *testing.T) { 1222 | reconciler := &controllers.CustomPodAutoscalerReconciler{ 1223 | Client: test.client, 1224 | Scheme: runtime.NewScheme(), 1225 | KubernetesResourceReconciler: test.k8sreconciler, 1226 | Log: logr.Discard(), 1227 | ScalingClient: test.scalingClient, 1228 | } 1229 | result, err := reconciler.Reconcile(context.Background(), test.request) 1230 | if !cmp.Equal(err, test.expectedErr, equateErrorMessage) { 1231 | t.Errorf("Error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) 1232 | return 1233 | } 1234 | 1235 | if !cmp.Equal(result, test.expected) { 1236 | t.Errorf("Result mismatch (-want +got):\n%s", cmp.Diff(result, test.expected)) 1237 | } 1238 | }) 1239 | } 1240 | } 1241 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jthomperoo/custom-pod-autoscaler-operator 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/go-logr/logr v1.4.1 7 | github.com/google/go-cmp v0.6.0 8 | honnef.co/go/tools v0.4.6 9 | k8s.io/api v0.29.1 10 | k8s.io/apimachinery v0.29.1 11 | k8s.io/client-go v0.29.1 12 | sigs.k8s.io/controller-runtime v0.17.1 13 | ) 14 | 15 | require ( 16 | github.com/BurntSushi/toml v1.3.2 // indirect 17 | github.com/beorn7/perks v1.0.1 // indirect 18 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/emicklei/go-restful/v3 v3.11.2 // indirect 21 | github.com/evanphx/json-patch v5.9.0+incompatible // indirect 22 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 23 | github.com/fsnotify/fsnotify v1.7.0 // indirect 24 | github.com/go-logr/zapr v1.3.0 // indirect 25 | github.com/go-openapi/jsonpointer v0.20.2 // indirect 26 | github.com/go-openapi/jsonreference v0.20.4 // indirect 27 | github.com/go-openapi/swag v0.22.9 // indirect 28 | github.com/gogo/protobuf v1.3.2 // indirect 29 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 30 | github.com/golang/protobuf v1.5.3 // indirect 31 | github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect 32 | github.com/google/gofuzz v1.2.0 // indirect 33 | github.com/google/uuid v1.6.0 // indirect 34 | github.com/imdario/mergo v0.3.6 // indirect 35 | github.com/josharian/intern v1.0.0 // indirect 36 | github.com/json-iterator/go v1.1.12 // indirect 37 | github.com/mailru/easyjson v0.7.7 // indirect 38 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 39 | github.com/modern-go/reflect2 v1.0.2 // indirect 40 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 41 | github.com/pkg/errors v0.9.1 // indirect 42 | github.com/prometheus/client_golang v1.18.0 // indirect 43 | github.com/prometheus/client_model v0.5.0 // indirect 44 | github.com/prometheus/common v0.46.0 // indirect 45 | github.com/prometheus/procfs v0.12.0 // indirect 46 | github.com/spf13/pflag v1.0.5 // indirect 47 | go.uber.org/multierr v1.11.0 // indirect 48 | go.uber.org/zap v1.26.0 // indirect 49 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect 50 | golang.org/x/exp/typeparams v0.0.0-20240205201215-2c58cdc269a3 // indirect 51 | golang.org/x/mod v0.15.0 // indirect 52 | golang.org/x/net v0.21.0 // indirect 53 | golang.org/x/oauth2 v0.17.0 // indirect 54 | golang.org/x/sys v0.17.0 // indirect 55 | golang.org/x/term v0.17.0 // indirect 56 | golang.org/x/text v0.14.0 // indirect 57 | golang.org/x/time v0.5.0 // indirect 58 | golang.org/x/tools v0.17.0 // indirect 59 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 60 | google.golang.org/appengine v1.6.8 // indirect 61 | google.golang.org/protobuf v1.32.0 // indirect 62 | gopkg.in/inf.v0 v0.9.1 // indirect 63 | gopkg.in/yaml.v2 v2.4.0 // indirect 64 | gopkg.in/yaml.v3 v3.0.1 // indirect 65 | k8s.io/apiextensions-apiserver v0.29.1 // indirect 66 | k8s.io/component-base v0.29.1 // indirect 67 | k8s.io/klog/v2 v2.120.1 // indirect 68 | k8s.io/kube-openapi v0.0.0-20240209001042-7a0d5b415232 // indirect 69 | k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect 70 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 71 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 72 | sigs.k8s.io/yaml v1.4.0 // indirect 73 | ) 74 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= 2 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 6 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU= 11 | github.com/emicklei/go-restful/v3 v3.11.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 12 | github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= 13 | github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 14 | github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= 15 | github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= 16 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 17 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 18 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 19 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 20 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 21 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 22 | github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= 23 | github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= 24 | github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= 25 | github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= 26 | github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= 27 | github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= 28 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 29 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 30 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 31 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 32 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 33 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 34 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 35 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 36 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 37 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 38 | github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU= 39 | github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M= 40 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 41 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 42 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 43 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 44 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 45 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 46 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 47 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= 48 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 49 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 50 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 51 | github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= 52 | github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 53 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 54 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 55 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 56 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 57 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 58 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 59 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 60 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 61 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 62 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 63 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 64 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 65 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 66 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 67 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 68 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 69 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 70 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 71 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 72 | github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= 73 | github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= 74 | github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= 75 | github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= 76 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 77 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 78 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 79 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 80 | github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= 81 | github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= 82 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 83 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 84 | github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= 85 | github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= 86 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 87 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 88 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 89 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 90 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 91 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 92 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 93 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 94 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 95 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 96 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 97 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 98 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 99 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 100 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 101 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 102 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 103 | go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 104 | go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 105 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 106 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 107 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 108 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 109 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= 110 | golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= 111 | golang.org/x/exp/typeparams v0.0.0-20240205201215-2c58cdc269a3 h1:1hsZWSQgrpqFJbqlg8HNhQ2/U/7IQELXYWTjuCfHNcM= 112 | golang.org/x/exp/typeparams v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= 113 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 114 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 115 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 116 | golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= 117 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 118 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 119 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 120 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 121 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 122 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 123 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 124 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 125 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 126 | golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= 127 | golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= 128 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 129 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 130 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 131 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 132 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 133 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 134 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 135 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 136 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 137 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 138 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 139 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 140 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 141 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= 142 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 143 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 144 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 145 | golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= 146 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 147 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 148 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 149 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 150 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 151 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 152 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 153 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 154 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 155 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 156 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 157 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 158 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 159 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 160 | golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= 161 | golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= 162 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 163 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 164 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 165 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 166 | gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= 167 | gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 168 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= 169 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= 170 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 171 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 172 | google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= 173 | google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 174 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 175 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 176 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 177 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 178 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 179 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 180 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 181 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 182 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 183 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 184 | honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8= 185 | honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= 186 | k8s.io/api v0.29.1 h1:DAjwWX/9YT7NQD4INu49ROJuZAAAP/Ijki48GUPzxqw= 187 | k8s.io/api v0.29.1/go.mod h1:7Kl10vBRUXhnQQI8YR/R327zXC8eJ7887/+Ybta+RoQ= 188 | k8s.io/apiextensions-apiserver v0.29.1 h1:S9xOtyk9M3Sk1tIpQMu9wXHm5O2MX6Y1kIpPMimZBZw= 189 | k8s.io/apiextensions-apiserver v0.29.1/go.mod h1:zZECpujY5yTW58co8V2EQR4BD6A9pktVgHhvc0uLfeU= 190 | k8s.io/apimachinery v0.29.1 h1:KY4/E6km/wLBguvCZv8cKTeOwwOBqFNjwJIdMkMbbRc= 191 | k8s.io/apimachinery v0.29.1/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= 192 | k8s.io/client-go v0.29.1 h1:19B/+2NGEwnFLzt0uB5kNJnfTsbV8w6TgQRz9l7ti7A= 193 | k8s.io/client-go v0.29.1/go.mod h1:TDG/psL9hdet0TI9mGyHJSgRkW3H9JZk2dNEUS7bRks= 194 | k8s.io/component-base v0.29.1 h1:MUimqJPCRnnHsskTTjKD+IC1EHBbRCVyi37IoFBrkYw= 195 | k8s.io/component-base v0.29.1/go.mod h1:fP9GFjxYrLERq1GcWWZAE3bqbNcDKDytn2srWuHTtKc= 196 | k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= 197 | k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 198 | k8s.io/kube-openapi v0.0.0-20240209001042-7a0d5b415232 h1:MMq4iF9pHuAz/9dLnHwBQKEoeigXClzs3MFh/seyqtA= 199 | k8s.io/kube-openapi v0.0.0-20240209001042-7a0d5b415232/go.mod h1:Pa1PvrP7ACSkuX6I7KYomY6cmMA0Tx86waBhDUgoKPw= 200 | k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ= 201 | k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 202 | sigs.k8s.io/controller-runtime v0.17.1 h1:V1dQELMGVk46YVXXQUbTFujU7u4DQj6YUj9Rb6cuzz8= 203 | sigs.k8s.io/controller-runtime v0.17.1/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= 204 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 205 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 206 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 207 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 208 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 209 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 210 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Custom Pod Autoscaler Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /helm/.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 | -------------------------------------------------------------------------------- /helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: custom-pod-autoscaler-operator 3 | description: The Custom Pod Autoscaler Operator allows deployment of Custom Pod Autoscalers 4 | 5 | type: application 6 | 7 | version: 0.0.0 8 | -------------------------------------------------------------------------------- /helm/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | Thanks for installing {{ .Chart.Name }}. 2 | 3 | Your release is named {{ .Release.Name }}. 4 | 5 | To find out more/request features/report bugs, check out the Custom Pod Autoscaler repos here: 6 | https://github.com/jthomperoo/custom-pod-autoscaler 7 | https://github.com/jthomperoo/custom-pod-autoscaler-operator 8 | -------------------------------------------------------------------------------- /helm/templates/cluster/cluster_role.yaml: -------------------------------------------------------------------------------- 1 | {{ if eq .Values.mode "cluster"}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | creationTimestamp: null 6 | name: {{ .Chart.Name }} 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - pods 12 | - services 13 | - services/finalizers 14 | - endpoints 15 | - persistentvolumeclaims 16 | - events 17 | - configmaps 18 | - secrets 19 | - serviceaccounts 20 | verbs: 21 | - '*' 22 | - apiGroups: 23 | - rbac.authorization.k8s.io 24 | resources: 25 | - roles 26 | - rolebindings 27 | verbs: 28 | - '*' 29 | - apiGroups: 30 | - apps 31 | resources: 32 | - deployments 33 | - deployments/scale 34 | - daemonsets 35 | - replicasets 36 | - statefulsets 37 | verbs: 38 | - '*' 39 | - apiGroups: 40 | - argoproj.io 41 | resources: 42 | - rollouts 43 | verbs: 44 | - '*' 45 | - apiGroups: 46 | - monitoring.coreos.com 47 | resources: 48 | - servicemonitors 49 | verbs: 50 | - get 51 | - create 52 | - apiGroups: 53 | - apps 54 | resourceNames: 55 | - custom-pod-autoscaler-operator 56 | resources: 57 | - deployments/finalizers 58 | verbs: 59 | - update 60 | - apiGroups: 61 | - custompodautoscaler.com 62 | resources: 63 | - '*' 64 | verbs: 65 | - '*' 66 | {{ end }} 67 | -------------------------------------------------------------------------------- /helm/templates/cluster/cluster_role_binding.yaml: -------------------------------------------------------------------------------- 1 | {{ if eq .Values.mode "cluster"}} 2 | kind: ClusterRoleBinding 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: {{ .Chart.Name }} 6 | subjects: 7 | - kind: ServiceAccount 8 | name: {{ .Chart.Name }}-cluster 9 | namespace: {{ .Release.Namespace }} 10 | roleRef: 11 | kind: ClusterRole 12 | name: {{ .Chart.Name }} 13 | apiGroup: rbac.authorization.k8s.io 14 | {{ end }} 15 | -------------------------------------------------------------------------------- /helm/templates/cluster/operator.yaml: -------------------------------------------------------------------------------- 1 | {{ if eq .Values.mode "cluster"}} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ .Chart.Name }} 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | name: {{ .Chart.Name }} 11 | template: 12 | metadata: 13 | labels: 14 | name: {{ .Chart.Name }} 15 | spec: 16 | serviceAccountName: {{ .Chart.Name }}-cluster 17 | containers: 18 | - name: {{ .Chart.Name }} 19 | image: "custompodautoscaler/operator:{{ .Chart.Version }}" 20 | imagePullPolicy: IfNotPresent 21 | env: 22 | - name: WATCH_NAMESPACE 23 | value: "" 24 | - name: POD_NAME 25 | valueFrom: 26 | fieldRef: 27 | fieldPath: metadata.name 28 | - name: OPERATOR_NAME 29 | value: "custom-pod-autoscaler-operator" 30 | {{ end }} 31 | -------------------------------------------------------------------------------- /helm/templates/cluster/service_account.yaml: -------------------------------------------------------------------------------- 1 | {{ if eq .Values.mode "cluster"}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: custom-pod-autoscaler-operator-cluster 6 | {{ end }} 7 | -------------------------------------------------------------------------------- /helm/templates/namespace/operator.yaml: -------------------------------------------------------------------------------- 1 | {{ if eq .Values.mode "namespaced"}} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ .Chart.Name }} 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | name: {{ .Chart.Name }} 11 | template: 12 | metadata: 13 | labels: 14 | name: {{ .Chart.Name }} 15 | spec: 16 | serviceAccountName: {{ .Chart.Name }} 17 | containers: 18 | - name: {{ .Chart.Name }} 19 | image: "custompodautoscaler/operator:{{ .Chart.Version }}" 20 | imagePullPolicy: IfNotPresent 21 | env: 22 | - name: WATCH_NAMESPACE 23 | valueFrom: 24 | fieldRef: 25 | fieldPath: metadata.namespace 26 | - name: POD_NAME 27 | valueFrom: 28 | fieldRef: 29 | fieldPath: metadata.name 30 | - name: OPERATOR_NAME 31 | value: "custom-pod-autoscaler-operator" 32 | {{ end }} 33 | -------------------------------------------------------------------------------- /helm/templates/namespace/role.yaml: -------------------------------------------------------------------------------- 1 | {{ if eq .Values.mode "namespaced"}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | creationTimestamp: null 6 | name: {{ .Chart.Name }} 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - pods 12 | - services 13 | - services/finalizers 14 | - endpoints 15 | - persistentvolumeclaims 16 | - events 17 | - configmaps 18 | - secrets 19 | - serviceaccounts 20 | verbs: 21 | - '*' 22 | - apiGroups: 23 | - rbac.authorization.k8s.io 24 | resources: 25 | - roles 26 | - rolebindings 27 | verbs: 28 | - '*' 29 | - apiGroups: 30 | - apps 31 | resources: 32 | - deployments 33 | - deployments/scale 34 | - daemonsets 35 | - replicasets 36 | - statefulsets 37 | verbs: 38 | - '*' 39 | - apiGroups: 40 | - argoproj.io 41 | resources: 42 | - rollouts 43 | verbs: 44 | - '*' 45 | - apiGroups: 46 | - monitoring.coreos.com 47 | resources: 48 | - servicemonitors 49 | verbs: 50 | - get 51 | - create 52 | - apiGroups: 53 | - apps 54 | resourceNames: 55 | - custom-pod-autoscaler-operator 56 | resources: 57 | - deployments/finalizers 58 | verbs: 59 | - update 60 | - apiGroups: 61 | - custompodautoscaler.com 62 | resources: 63 | - '*' 64 | verbs: 65 | - '*' 66 | {{ end }} 67 | -------------------------------------------------------------------------------- /helm/templates/namespace/role_binding.yaml: -------------------------------------------------------------------------------- 1 | {{ if eq .Values.mode "namespaced"}} 2 | kind: RoleBinding 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: {{ .Chart.Name }} 6 | subjects: 7 | - kind: ServiceAccount 8 | name: {{ .Chart.Name }} 9 | roleRef: 10 | kind: Role 11 | name: {{ .Chart.Name }} 12 | apiGroup: rbac.authorization.k8s.io 13 | {{ end }} 14 | -------------------------------------------------------------------------------- /helm/templates/namespace/service_account.yaml: -------------------------------------------------------------------------------- 1 | {{ if eq .Values.mode "namespaced"}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ .Chart.Name }} 6 | {{ end }} 7 | -------------------------------------------------------------------------------- /helm/values.yaml: -------------------------------------------------------------------------------- 1 | mode: cluster -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Custom Pod Autoscaler Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "os" 21 | 22 | "k8s.io/apimachinery/pkg/runtime" 23 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 24 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 25 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 26 | ctrl "sigs.k8s.io/controller-runtime" 27 | "sigs.k8s.io/controller-runtime/pkg/cache" 28 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 29 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 30 | "sigs.k8s.io/controller-runtime/pkg/metrics/server" 31 | 32 | custompodautoscalercomv1 "github.com/jthomperoo/custom-pod-autoscaler-operator/api/v1" 33 | "github.com/jthomperoo/custom-pod-autoscaler-operator/controllers" 34 | "github.com/jthomperoo/custom-pod-autoscaler-operator/reconcile" 35 | // +kubebuilder:scaffold:imports 36 | ) 37 | 38 | const watchNamespaceEnvVar = "WATCH_NAMESPACE" 39 | 40 | var ( 41 | scheme = runtime.NewScheme() 42 | setupLog = ctrl.Log.WithName("setup") 43 | ) 44 | 45 | func init() { 46 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 47 | 48 | utilruntime.Must(custompodautoscalercomv1.AddToScheme(scheme)) 49 | // +kubebuilder:scaffold:scheme 50 | } 51 | 52 | func main() { 53 | namespace := os.Getenv(watchNamespaceEnvVar) 54 | 55 | ctrl.SetLogger(zap.New(zap.UseDevMode(true))) 56 | 57 | var namespacedCache = cache.Options{} 58 | if namespace != "" { 59 | namespacedCache.DefaultNamespaces = map[string]cache.Config{ 60 | namespace: {}, 61 | } 62 | } 63 | 64 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 65 | Scheme: scheme, 66 | Metrics: server.Options{ 67 | BindAddress: ":8000", 68 | }, 69 | Cache: namespacedCache, 70 | }) 71 | if err != nil { 72 | setupLog.Error(err, "unable to start manager") 73 | os.Exit(1) 74 | } 75 | 76 | client := mgr.GetClient() 77 | scheme := mgr.GetScheme() 78 | scalingClient, err := controllers.SetupScalingClient() 79 | if err != nil { 80 | setupLog.Error(err, "unable to set up scaling client") 81 | os.Exit(1) 82 | } 83 | 84 | if err = (&controllers.CustomPodAutoscalerReconciler{ 85 | Client: client, 86 | Log: ctrl.Log.WithName("controllers").WithName("CustomPodAutoscaler"), 87 | Scheme: scheme, 88 | KubernetesResourceReconciler: &reconcile.KubernetesResourceReconciler{ 89 | Client: client, 90 | Scheme: scheme, 91 | ControllerReferencer: controllerutil.SetControllerReference, 92 | }, 93 | ScalingClient: scalingClient, 94 | }).SetupWithManager(mgr); err != nil { 95 | setupLog.Error(err, "unable to create controller", "controller", "CustomPodAutoscaler") 96 | os.Exit(1) 97 | } 98 | // +kubebuilder:scaffold:builder 99 | 100 | setupLog.Info("starting manager") 101 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 102 | setupLog.Error(err, "problem running manager") 103 | os.Exit(1) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /reconcile/reconcile.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Custom Pod Autoscaler Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package reconcile 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/go-logr/logr" 23 | custompodautoscalercomv1 "github.com/jthomperoo/custom-pod-autoscaler-operator/api/v1" 24 | "github.com/jthomperoo/custom-pod-autoscaler-operator/controllers" 25 | corev1 "k8s.io/api/core/v1" 26 | "k8s.io/apimachinery/pkg/api/errors" 27 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | "k8s.io/apimachinery/pkg/runtime" 29 | "k8s.io/apimachinery/pkg/types" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 32 | ) 33 | 34 | type controllerReferencer func(owner, object metav1.Object, scheme *runtime.Scheme) error 35 | 36 | // KubernetesResourceReconciler handles reconciling Kubernetes resources, such as pods, service accounts etc. 37 | type KubernetesResourceReconciler struct { 38 | Scheme *runtime.Scheme 39 | Client client.Client 40 | ControllerReferencer controllerReferencer 41 | } 42 | 43 | // Reconcile manages k8s objects, making sure that the supplied object exists, and if it 44 | // doesn't it creates one 45 | func (k *KubernetesResourceReconciler) Reconcile( 46 | reqLogger logr.Logger, 47 | instance *custompodautoscalercomv1.CustomPodAutoscaler, 48 | obj metav1.Object, 49 | shouldProvision bool, 50 | updatable bool, 51 | kind string, 52 | ) (reconcile.Result, error) { 53 | runtimeObj := obj.(client.Object) 54 | // Set CustomPodAutoscaler instance as the owner and controller 55 | err := k.ControllerReferencer(instance, obj, k.Scheme) 56 | if err != nil { 57 | return reconcile.Result{}, err 58 | } 59 | 60 | // Check if k8s object already exists 61 | existingObject := runtimeObj 62 | err = k.Client.Get(context.Background(), types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, existingObject) 63 | if err != nil { 64 | if !errors.IsNotFound(err) { 65 | return reconcile.Result{}, err 66 | } 67 | // Object does not exist 68 | if !shouldProvision { 69 | reqLogger.Info("Object not found, no provisioning of resource ", "Kind", kind, "Namespace", obj.GetNamespace(), "Name", obj.GetName()) 70 | // Should not provision a new object, wait for existing 71 | return reconcile.Result{}, nil 72 | } 73 | // Should provision, create a new object 74 | reqLogger.Info("Creating a new k8s object ", "Kind", kind, "Namespace", obj.GetNamespace(), "Name", obj.GetName()) 75 | err = k.Client.Create(context.Background(), runtimeObj) 76 | if err != nil { 77 | return reconcile.Result{}, err 78 | } 79 | // K8s object created successfully - don't requeue 80 | return reconcile.Result{}, nil 81 | } 82 | 83 | if existingObject.GetObjectKind().GroupVersionKind().Group == "" && 84 | existingObject.GetObjectKind().GroupVersionKind().Version == "v1" && 85 | existingObject.GetObjectKind().GroupVersionKind().Kind == "Pod" { 86 | pod := existingObject.(*corev1.Pod) 87 | if !pod.ObjectMeta.DeletionTimestamp.IsZero() { 88 | reqLogger.Info("Pod currently being deleted ", "Kind", kind, "Namespace", obj.GetNamespace(), "Name", obj.GetName()) 89 | return reconcile.Result{}, nil 90 | } 91 | } 92 | 93 | // Object already exists, update 94 | if shouldProvision { 95 | // Only update if object should be provisioned 96 | if updatable { 97 | reqLogger.Info("Updating k8s object ", "Kind", kind, "Namespace", obj.GetNamespace(), "Name", obj.GetName()) 98 | if existingObject.GetObjectKind().GroupVersionKind().Group == "" && 99 | existingObject.GetObjectKind().GroupVersionKind().Version == "v1" && 100 | existingObject.GetObjectKind().GroupVersionKind().Kind == "ServiceAccount" { 101 | reqLogger.Info("Service Account update, retaining secrets ", "Kind", kind, "Namespace", obj.GetNamespace(), "Name", obj.GetName()) 102 | serviceAccount := existingObject.(*corev1.ServiceAccount) 103 | updatedServiceAccount := runtimeObj.(*corev1.ServiceAccount) 104 | updatedServiceAccount.Secrets = serviceAccount.Secrets 105 | } 106 | // If object can be updated 107 | err = k.Client.Update(context.Background(), runtimeObj) 108 | if err != nil { 109 | return reconcile.Result{}, err 110 | } 111 | // Successful update, don't requeue 112 | return reconcile.Result{}, nil 113 | } 114 | reqLogger.Info("Deleting k8s object ", "Kind", kind, "Namespace", obj.GetNamespace(), "Name", obj.GetName()) 115 | 116 | // If object can't be updated, delete and make new 117 | err = k.Client.Delete(context.Background(), existingObject) 118 | if err != nil { 119 | return reconcile.Result{}, err 120 | } 121 | 122 | return reconcile.Result{}, nil 123 | } 124 | 125 | // Object should not be provisioned, instead update owner reference of 126 | // existing object 127 | obj = existingObject.(metav1.Object) 128 | // Check if CPA set as K8s object owner 129 | ownerReferences := obj.GetOwnerReferences() 130 | cpaOwner := false 131 | for _, owner := range ownerReferences { 132 | if owner.Kind == instance.Kind && owner.APIVersion == instance.APIVersion && owner.Name == instance.Name { 133 | cpaOwner = true 134 | break 135 | } 136 | } 137 | 138 | if !cpaOwner { 139 | reqLogger.Info("CPA not set as owner, updating owner reference", "Kind", kind, "Namespace", obj.GetNamespace(), "Name", obj.GetName()) 140 | ownerReferences = append(ownerReferences, metav1.OwnerReference{ 141 | APIVersion: instance.APIVersion, 142 | Kind: instance.Kind, 143 | Name: instance.Name, 144 | UID: instance.UID, 145 | }) 146 | obj.SetOwnerReferences(ownerReferences) 147 | err = k.Client.Update(context.Background(), existingObject) 148 | if err != nil { 149 | return reconcile.Result{}, err 150 | } 151 | return reconcile.Result{}, nil 152 | } 153 | 154 | reqLogger.Info("Skip reconcile: k8s object already exists with expected owner", "Kind", kind, "Namespace", obj.GetNamespace(), "Name", obj.GetName()) 155 | return reconcile.Result{}, nil 156 | } 157 | 158 | // PodCleanup will look for any Pods that have the v1.custompodautoscaler.com/owned-by label set to the name of the CPA 159 | // and delete any 'orphaned' Pods, these are Pods that are owned by the CPA but are no longer defined in the CPA 160 | // PodTemplateSpec (for example if the PodTemplateSpec has renamed the Pod, it should delete the old Pod as it 161 | // provisions a new Pod so there aren't two Pods for the CPA) 162 | func (k *KubernetesResourceReconciler) PodCleanup(reqLogger logr.Logger, instance *custompodautoscalercomv1.CustomPodAutoscaler) error { 163 | pods := &corev1.PodList{} 164 | err := k.Client.List(context.Background(), pods, 165 | client.MatchingLabels{controllers.OwnedByLabel: instance.Name}, 166 | client.InNamespace(instance.Namespace)) 167 | 168 | if err != nil { 169 | return err 170 | } 171 | 172 | for _, pod := range pods.Items { 173 | managed := false 174 | for _, ownerRef := range pod.OwnerReferences { 175 | if ownerRef.APIVersion != instance.APIVersion || ownerRef.Kind != instance.Kind || ownerRef.Name != instance.Name { 176 | continue 177 | } 178 | 179 | managed = true 180 | } 181 | 182 | if !managed { 183 | continue 184 | } 185 | 186 | if instance.Spec.Template.ObjectMeta.Name == "" { 187 | // Using instance name, delete any pod that isn't using the instance name 188 | if pod.Name == instance.Name { 189 | continue 190 | } 191 | 192 | err = k.deleteOrphan(reqLogger, pod) 193 | if err != nil { 194 | return err 195 | } 196 | continue 197 | } 198 | 199 | // Using name defined in template, delete any pod that doesn't match that name 200 | if pod.Name != instance.Spec.Template.ObjectMeta.Name { 201 | err = k.deleteOrphan(reqLogger, pod) 202 | if err != nil { 203 | return err 204 | } 205 | } 206 | } 207 | 208 | return nil 209 | } 210 | 211 | func (k *KubernetesResourceReconciler) deleteOrphan(reqLogger logr.Logger, pod corev1.Pod) error { 212 | reqLogger.Info("Found orphaned Pod (owned by CPA but not currently defined), deleting", "Kind", pod.GetObjectKind().GroupVersionKind(), "Namespace", pod.GetNamespace(), "Name", pod.GetName()) 213 | return k.Client.Delete(context.Background(), &pod) 214 | } 215 | -------------------------------------------------------------------------------- /reconcile/reconcile_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Custom Pod Autoscaler Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package reconcile_test 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "testing" 23 | "time" 24 | 25 | "github.com/go-logr/logr" 26 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 27 | 28 | "github.com/google/go-cmp/cmp" 29 | custompodautoscalercomv1 "github.com/jthomperoo/custom-pod-autoscaler-operator/api/v1" 30 | k8sreconcile "github.com/jthomperoo/custom-pod-autoscaler-operator/reconcile" 31 | corev1 "k8s.io/api/core/v1" 32 | apierrors "k8s.io/apimachinery/pkg/api/errors" 33 | "k8s.io/apimachinery/pkg/api/meta" 34 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 | "k8s.io/apimachinery/pkg/runtime" 36 | "k8s.io/apimachinery/pkg/runtime/schema" 37 | "sigs.k8s.io/controller-runtime/pkg/client" 38 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 39 | ) 40 | 41 | var log = logr.Discard() 42 | 43 | type fakeClient struct { 44 | get func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error 45 | list func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error 46 | create func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error 47 | delete func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error 48 | update func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error 49 | patch func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error 50 | deleteAllOf func(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error 51 | status func() client.StatusWriter 52 | scheme func() *runtime.Scheme 53 | restMapper func() meta.RESTMapper 54 | groupVersionKindFor func(obj runtime.Object) (schema.GroupVersionKind, error) 55 | isObjectNamespaced func(obj runtime.Object) (bool, error) 56 | subResource func(subResource string) client.SubResourceClient 57 | } 58 | 59 | func (f *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 60 | return f.get(ctx, key, obj) 61 | } 62 | 63 | func (f *fakeClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { 64 | return f.list(ctx, list, opts...) 65 | } 66 | 67 | func (f *fakeClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { 68 | return f.create(ctx, obj, opts...) 69 | } 70 | 71 | func (f *fakeClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { 72 | return f.delete(ctx, obj, opts...) 73 | } 74 | 75 | func (f *fakeClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 76 | return f.update(ctx, obj, opts...) 77 | } 78 | 79 | func (f *fakeClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { 80 | return f.patch(ctx, obj, patch, opts...) 81 | } 82 | 83 | func (f *fakeClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { 84 | return f.deleteAllOf(ctx, obj, opts...) 85 | } 86 | 87 | func (f *fakeClient) Status() client.StatusWriter { 88 | return f.status() 89 | } 90 | 91 | func (f *fakeClient) Scheme() *runtime.Scheme { 92 | return f.scheme() 93 | } 94 | 95 | func (f *fakeClient) RESTMapper() meta.RESTMapper { 96 | return f.restMapper() 97 | } 98 | 99 | func (f *fakeClient) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { 100 | return f.groupVersionKindFor(obj) 101 | } 102 | 103 | func (f *fakeClient) IsObjectNamespaced(obj runtime.Object) (bool, error) { 104 | return f.isObjectNamespaced(obj) 105 | } 106 | 107 | func (f *fakeClient) SubResource(subResource string) client.SubResourceClient { 108 | return f.subResource(subResource) 109 | } 110 | 111 | func TestReconcile(t *testing.T) { 112 | equateErrorMessage := cmp.Comparer(func(x, y error) bool { 113 | if x == nil || y == nil { 114 | return x == nil && y == nil 115 | } 116 | return x.Error() == y.Error() 117 | }) 118 | 119 | var tests = []struct { 120 | description string 121 | expected reconcile.Result 122 | expectedErr error 123 | reconciler *k8sreconcile.KubernetesResourceReconciler 124 | logger logr.Logger 125 | instance *custompodautoscalercomv1.CustomPodAutoscaler 126 | obj metav1.Object 127 | shouldProvision bool 128 | updatable bool 129 | kind string 130 | }{ 131 | { 132 | "Fail to set controller reference", 133 | reconcile.Result{}, 134 | errors.New("Fail to set controller reference"), 135 | &k8sreconcile.KubernetesResourceReconciler{ 136 | Client: nil, 137 | Scheme: &runtime.Scheme{}, 138 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 139 | return errors.New("Fail to set controller reference") 140 | }, 141 | }, 142 | log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 143 | &custompodautoscalercomv1.CustomPodAutoscaler{}, 144 | &corev1.Pod{ 145 | ObjectMeta: metav1.ObjectMeta{ 146 | Name: "test pod", 147 | Namespace: "test namespace", 148 | }, 149 | }, 150 | true, 151 | false, 152 | "v1/Pod", 153 | }, 154 | { 155 | "Fail to get object", 156 | reconcile.Result{}, 157 | errors.New("Fail to get object"), 158 | &k8sreconcile.KubernetesResourceReconciler{ 159 | Client: func() *fakeClient { 160 | fclient := &fakeClient{} 161 | // Client fails to get object 162 | fclient.get = func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 163 | return errors.New("Fail to get object") 164 | } 165 | return fclient 166 | }(), 167 | Scheme: &runtime.Scheme{}, 168 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 169 | return nil 170 | }, 171 | }, 172 | log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 173 | &custompodautoscalercomv1.CustomPodAutoscaler{}, 174 | &corev1.Pod{ 175 | ObjectMeta: metav1.ObjectMeta{ 176 | Name: "test pod", 177 | Namespace: "test namespace", 178 | }, 179 | }, 180 | true, 181 | false, 182 | "v1/Pod", 183 | }, 184 | { 185 | "Fail to create object", 186 | reconcile.Result{}, 187 | errors.New("Fail to create object"), 188 | &k8sreconcile.KubernetesResourceReconciler{ 189 | Client: func() *fakeClient { 190 | fclient := &fakeClient{} 191 | // Client reports object not found 192 | fclient.get = func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 193 | return apierrors.NewNotFound(schema.GroupResource{}, key.Namespace) 194 | } 195 | // Creation fails 196 | fclient.create = func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { 197 | return errors.New("Fail to create object") 198 | } 199 | return fclient 200 | }(), 201 | Scheme: &runtime.Scheme{}, 202 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 203 | return nil 204 | }, 205 | }, 206 | log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 207 | &custompodautoscalercomv1.CustomPodAutoscaler{}, 208 | &corev1.Pod{ 209 | ObjectMeta: metav1.ObjectMeta{ 210 | Name: "test pod", 211 | Namespace: "test namespace", 212 | }, 213 | }, 214 | true, 215 | false, 216 | "v1/Pod", 217 | }, 218 | { 219 | "Success, no object found and don't provision a new one", 220 | reconcile.Result{}, 221 | nil, 222 | &k8sreconcile.KubernetesResourceReconciler{ 223 | Client: func() *fakeClient { 224 | fclient := &fakeClient{} 225 | // Client reports object not found 226 | fclient.get = func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 227 | return apierrors.NewNotFound(schema.GroupResource{}, key.Namespace) 228 | } 229 | return fclient 230 | }(), 231 | Scheme: &runtime.Scheme{}, 232 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 233 | return nil 234 | }, 235 | }, 236 | log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 237 | &custompodautoscalercomv1.CustomPodAutoscaler{}, 238 | &corev1.Pod{ 239 | ObjectMeta: metav1.ObjectMeta{ 240 | Name: "test pod", 241 | Namespace: "test namespace", 242 | }, 243 | }, 244 | false, 245 | false, 246 | "v1/Pod", 247 | }, 248 | { 249 | "Successfully create new object", 250 | reconcile.Result{}, 251 | nil, 252 | &k8sreconcile.KubernetesResourceReconciler{ 253 | Client: func() *fakeClient { 254 | fclient := &fakeClient{} 255 | // Client reports object not found 256 | fclient.get = func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 257 | return apierrors.NewNotFound(schema.GroupResource{}, key.Namespace) 258 | } 259 | // Creation successful 260 | fclient.create = func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { 261 | return nil 262 | } 263 | return fclient 264 | }(), 265 | Scheme: &runtime.Scheme{}, 266 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 267 | return nil 268 | }, 269 | }, 270 | log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 271 | &custompodautoscalercomv1.CustomPodAutoscaler{}, 272 | &corev1.Pod{ 273 | ObjectMeta: metav1.ObjectMeta{ 274 | Name: "test pod", 275 | Namespace: "test namespace", 276 | }, 277 | }, 278 | true, 279 | false, 280 | "v1/Pod", 281 | }, 282 | { 283 | "Object already exists; Pod being deleted, skip updating", 284 | reconcile.Result{}, 285 | nil, 286 | &k8sreconcile.KubernetesResourceReconciler{ 287 | Client: func() *fakeClient { 288 | fclient := &fakeClient{} 289 | fclient.get = func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 290 | return nil 291 | } 292 | return fclient 293 | }(), 294 | Scheme: &runtime.Scheme{}, 295 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 296 | return nil 297 | }, 298 | }, 299 | log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 300 | &custompodautoscalercomv1.CustomPodAutoscaler{}, 301 | &corev1.Pod{ 302 | ObjectMeta: metav1.ObjectMeta{ 303 | Name: "test pod", 304 | Namespace: "test namespace", 305 | DeletionTimestamp: &metav1.Time{ 306 | Time: time.Now(), 307 | }, 308 | }, 309 | TypeMeta: metav1.TypeMeta{ 310 | Kind: "Pod", 311 | APIVersion: "v1", 312 | }, 313 | }, 314 | true, 315 | false, 316 | "v1/Pod", 317 | }, 318 | { 319 | "Object already exists; should be provisioned and is updatable, fail to update", 320 | reconcile.Result{}, 321 | errors.New("Fail to update"), 322 | &k8sreconcile.KubernetesResourceReconciler{ 323 | Client: func() *fakeClient { 324 | fclient := &fakeClient{} 325 | fclient.get = func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 326 | return nil 327 | } 328 | // Fail to update 329 | fclient.update = func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 330 | return errors.New("Fail to update") 331 | } 332 | return fclient 333 | }(), 334 | Scheme: &runtime.Scheme{}, 335 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 336 | return nil 337 | }, 338 | }, 339 | log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 340 | &custompodautoscalercomv1.CustomPodAutoscaler{}, 341 | &corev1.ServiceAccount{ 342 | ObjectMeta: metav1.ObjectMeta{ 343 | Name: "test sa", 344 | Namespace: "test namespace", 345 | }, 346 | }, 347 | true, 348 | true, 349 | "v1/ServiceAccount", 350 | }, 351 | { 352 | "Object already exists; should be provisioned and is updatable, update success", 353 | reconcile.Result{}, 354 | nil, 355 | &k8sreconcile.KubernetesResourceReconciler{ 356 | Client: func() *fakeClient { 357 | fclient := &fakeClient{} 358 | fclient.get = func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 359 | return nil 360 | } 361 | // Fail to update 362 | fclient.update = func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 363 | return nil 364 | } 365 | return fclient 366 | }(), 367 | Scheme: &runtime.Scheme{}, 368 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 369 | return nil 370 | }, 371 | }, 372 | log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 373 | &custompodautoscalercomv1.CustomPodAutoscaler{}, 374 | &corev1.ServiceAccount{ 375 | ObjectMeta: metav1.ObjectMeta{ 376 | Name: "test sa", 377 | Namespace: "test namespace", 378 | }, 379 | }, 380 | true, 381 | true, 382 | "v1/ServiceAccount", 383 | }, 384 | { 385 | "Object already exists; should be provisioned and isn't updatable, fail to delete", 386 | reconcile.Result{}, 387 | errors.New("Fail to delete"), 388 | &k8sreconcile.KubernetesResourceReconciler{ 389 | Client: func() *fakeClient { 390 | fclient := &fakeClient{} 391 | fclient.get = func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 392 | return nil 393 | } 394 | // Fail to delete 395 | fclient.delete = func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { 396 | return errors.New("Fail to delete") 397 | } 398 | return fclient 399 | }(), 400 | Scheme: &runtime.Scheme{}, 401 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 402 | return nil 403 | }, 404 | }, 405 | log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 406 | &custompodautoscalercomv1.CustomPodAutoscaler{}, 407 | &corev1.ServiceAccount{ 408 | ObjectMeta: metav1.ObjectMeta{ 409 | Name: "test sa", 410 | Namespace: "test namespace", 411 | }, 412 | }, 413 | true, 414 | false, 415 | "v1/ServiceAccount", 416 | }, 417 | { 418 | "Object already exists; should be provisioned and isn't updatable, delete success", 419 | reconcile.Result{}, 420 | nil, 421 | &k8sreconcile.KubernetesResourceReconciler{ 422 | Client: func() *fakeClient { 423 | fclient := &fakeClient{} 424 | fclient.get = func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 425 | return nil 426 | } 427 | // Fail to delete 428 | fclient.delete = func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { 429 | return nil 430 | } 431 | return fclient 432 | }(), 433 | Scheme: &runtime.Scheme{}, 434 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 435 | return nil 436 | }, 437 | }, 438 | log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 439 | &custompodautoscalercomv1.CustomPodAutoscaler{}, 440 | &corev1.ServiceAccount{ 441 | ObjectMeta: metav1.ObjectMeta{ 442 | Name: "test sa", 443 | Namespace: "test namespace", 444 | }, 445 | }, 446 | true, 447 | false, 448 | "v1/ServiceAccount", 449 | }, 450 | { 451 | "Object already exists with owner not set, fail to update", 452 | reconcile.Result{}, 453 | errors.New("Fail to update object"), 454 | &k8sreconcile.KubernetesResourceReconciler{ 455 | Client: func() *fakeClient { 456 | fclient := &fakeClient{} 457 | fclient.get = func(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 458 | return nil 459 | } 460 | // Update fails 461 | fclient.update = func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 462 | return errors.New("Fail to update object") 463 | } 464 | return fclient 465 | }(), 466 | Scheme: &runtime.Scheme{}, 467 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 468 | return nil 469 | }, 470 | }, 471 | log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 472 | &custompodautoscalercomv1.CustomPodAutoscaler{ 473 | TypeMeta: metav1.TypeMeta{ 474 | Kind: "custompodautoscaler", 475 | APIVersion: "custompodautoscaler.com/v1", 476 | }, 477 | ObjectMeta: metav1.ObjectMeta{ 478 | Name: "testcpa", 479 | UID: "testuid", 480 | }, 481 | }, 482 | &corev1.ServiceAccount{ 483 | ObjectMeta: metav1.ObjectMeta{ 484 | Name: "test sa", 485 | Namespace: "test namespace", 486 | }, 487 | }, 488 | false, 489 | false, 490 | "v1/ServiceAccount", 491 | }, 492 | { 493 | "Object already exists with owner not set, successful update", 494 | reconcile.Result{}, 495 | nil, 496 | &k8sreconcile.KubernetesResourceReconciler{ 497 | Client: fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 498 | s := runtime.NewScheme() 499 | s.AddKnownTypes(schema.GroupVersion{ 500 | Group: "", 501 | Version: "v1", 502 | }, &corev1.Pod{}) 503 | return s 504 | }()).WithRuntimeObjects( 505 | &corev1.Pod{ 506 | ObjectMeta: metav1.ObjectMeta{ 507 | Name: "test pod", 508 | Namespace: "test namespace", 509 | }, 510 | }, 511 | ).Build(), 512 | Scheme: &runtime.Scheme{}, 513 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 514 | return nil 515 | }, 516 | }, 517 | log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 518 | &custompodautoscalercomv1.CustomPodAutoscaler{ 519 | TypeMeta: metav1.TypeMeta{ 520 | Kind: "custompodautoscaler", 521 | APIVersion: "custompodautoscaler.com/v1", 522 | }, 523 | ObjectMeta: metav1.ObjectMeta{ 524 | Name: "testcpa", 525 | UID: "testuid", 526 | }, 527 | }, 528 | &corev1.Pod{ 529 | ObjectMeta: metav1.ObjectMeta{ 530 | Name: "test pod", 531 | Namespace: "test namespace", 532 | }, 533 | }, 534 | false, 535 | false, 536 | "v1/Pod", 537 | }, 538 | { 539 | "Object already exists with owner set", 540 | reconcile.Result{}, 541 | nil, 542 | &k8sreconcile.KubernetesResourceReconciler{ 543 | Client: fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 544 | s := runtime.NewScheme() 545 | s.AddKnownTypes(schema.GroupVersion{ 546 | Group: "", 547 | Version: "v1", 548 | }, &corev1.Pod{}) 549 | return s 550 | }()).WithRuntimeObjects( 551 | &corev1.Pod{ 552 | ObjectMeta: metav1.ObjectMeta{ 553 | Name: "test pod", 554 | Namespace: "test namespace", 555 | OwnerReferences: []metav1.OwnerReference{ 556 | { 557 | Kind: "custompodautoscaler", 558 | APIVersion: "custompodautoscaler.com/v1", 559 | Name: "testcpa", 560 | UID: "testuid", 561 | }, 562 | }, 563 | }, 564 | }, 565 | ).Build(), 566 | Scheme: &runtime.Scheme{}, 567 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 568 | return nil 569 | }, 570 | }, 571 | log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 572 | &custompodautoscalercomv1.CustomPodAutoscaler{ 573 | TypeMeta: metav1.TypeMeta{ 574 | Kind: "custompodautoscaler", 575 | APIVersion: "custompodautoscaler.com/v1", 576 | }, 577 | ObjectMeta: metav1.ObjectMeta{ 578 | Name: "testcpa", 579 | UID: "testuid", 580 | }, 581 | }, 582 | &corev1.Pod{ 583 | ObjectMeta: metav1.ObjectMeta{ 584 | Name: "test pod", 585 | Namespace: "test namespace", 586 | OwnerReferences: []metav1.OwnerReference{ 587 | { 588 | Kind: "custompodautoscaler", 589 | APIVersion: "custompodautoscaler.com/v1", 590 | Name: "testcpa", 591 | UID: "testuid", 592 | }, 593 | }, 594 | }, 595 | }, 596 | false, 597 | false, 598 | "v1/Pod", 599 | }, 600 | { 601 | "Service account already exists, retain secret", 602 | reconcile.Result{}, 603 | nil, 604 | &k8sreconcile.KubernetesResourceReconciler{ 605 | Client: fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 606 | s := runtime.NewScheme() 607 | s.AddKnownTypes(schema.GroupVersion{ 608 | Group: "", 609 | Version: "v1", 610 | }, &corev1.ServiceAccount{}) 611 | return s 612 | }()).WithRuntimeObjects( 613 | &corev1.ServiceAccount{ 614 | TypeMeta: metav1.TypeMeta{ 615 | Kind: "ServiceAccount", 616 | APIVersion: "v1", 617 | }, 618 | ObjectMeta: metav1.ObjectMeta{ 619 | Name: "test sa", 620 | Namespace: "test namespace", 621 | }, 622 | }, 623 | ).Build(), 624 | Scheme: &runtime.Scheme{}, 625 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 626 | return nil 627 | }, 628 | }, 629 | log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 630 | &custompodautoscalercomv1.CustomPodAutoscaler{ 631 | TypeMeta: metav1.TypeMeta{ 632 | Kind: "custompodautoscaler", 633 | APIVersion: "custompodautoscaler.com/v1", 634 | }, 635 | ObjectMeta: metav1.ObjectMeta{ 636 | Name: "testcpa", 637 | UID: "testuid", 638 | }, 639 | }, 640 | &corev1.ServiceAccount{ 641 | TypeMeta: metav1.TypeMeta{ 642 | Kind: "ServiceAccount", 643 | APIVersion: "v1", 644 | }, 645 | ObjectMeta: metav1.ObjectMeta{ 646 | Name: "test sa", 647 | Namespace: "test namespace", 648 | OwnerReferences: []metav1.OwnerReference{ 649 | { 650 | Kind: "custompodautoscaler", 651 | APIVersion: "custompodautoscaler.com/v1", 652 | Name: "testcpa", 653 | UID: "testuid", 654 | }, 655 | }, 656 | }, 657 | }, 658 | true, 659 | true, 660 | "v1/ServiceAccount", 661 | }, 662 | } 663 | for _, test := range tests { 664 | t.Run(test.description, func(t *testing.T) { 665 | result, err := test.reconciler.Reconcile(test.logger, test.instance, test.obj, test.shouldProvision, test.updatable, test.kind) 666 | if !cmp.Equal(err, test.expectedErr, equateErrorMessage) { 667 | t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) 668 | return 669 | } 670 | 671 | if !cmp.Equal(result, test.expected) { 672 | t.Errorf("result mismatch (-want +got):\n%s", cmp.Diff(result, test.expected)) 673 | } 674 | }) 675 | } 676 | } 677 | 678 | func TestPodCleanup(t *testing.T) { 679 | equateErrorMessage := cmp.Comparer(func(x, y error) bool { 680 | if x == nil || y == nil { 681 | return x == nil && y == nil 682 | } 683 | return x.Error() == y.Error() 684 | }) 685 | 686 | var tests = []struct { 687 | description string 688 | expectedErr error 689 | reconciler *k8sreconcile.KubernetesResourceReconciler 690 | logger logr.Logger 691 | instance *custompodautoscalercomv1.CustomPodAutoscaler 692 | }{ 693 | { 694 | description: "Fail to list pods", 695 | expectedErr: errors.New("fail to list pods"), 696 | reconciler: &k8sreconcile.KubernetesResourceReconciler{ 697 | Client: func() *fakeClient { 698 | fclient := &fakeClient{} 699 | // Client fails to get object 700 | fclient.list = func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { 701 | return errors.New("fail to list pods") 702 | } 703 | return fclient 704 | }(), 705 | Scheme: &runtime.Scheme{}, 706 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 707 | return nil 708 | }, 709 | }, 710 | logger: log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 711 | instance: &custompodautoscalercomv1.CustomPodAutoscaler{ 712 | TypeMeta: metav1.TypeMeta{ 713 | Kind: "custompodautoscaler", 714 | APIVersion: "custompodautoscaler.com/v1", 715 | }, 716 | ObjectMeta: metav1.ObjectMeta{ 717 | Name: "testcpa", 718 | UID: "testuid", 719 | }, 720 | }, 721 | }, 722 | { 723 | description: "No pods found", 724 | expectedErr: nil, 725 | reconciler: &k8sreconcile.KubernetesResourceReconciler{ 726 | Client: fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 727 | s := runtime.NewScheme() 728 | s.AddKnownTypes(schema.GroupVersion{ 729 | Group: "", 730 | Version: "v1", 731 | }, &corev1.PodList{}) 732 | return s 733 | }()).Build(), 734 | Scheme: &runtime.Scheme{}, 735 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 736 | return nil 737 | }, 738 | }, 739 | logger: log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 740 | instance: &custompodautoscalercomv1.CustomPodAutoscaler{ 741 | TypeMeta: metav1.TypeMeta{ 742 | Kind: "custompodautoscaler", 743 | APIVersion: "custompodautoscaler.com/v1", 744 | }, 745 | ObjectMeta: metav1.ObjectMeta{ 746 | Name: "testcpa", 747 | UID: "testuid", 748 | }, 749 | }, 750 | }, 751 | { 752 | description: "Three pods, one owned by a different CPA, two not matching label", 753 | expectedErr: nil, 754 | reconciler: &k8sreconcile.KubernetesResourceReconciler{ 755 | Client: fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 756 | s := runtime.NewScheme() 757 | s.AddKnownTypes(schema.GroupVersion{ 758 | Group: "", 759 | Version: "v1", 760 | }, &corev1.PodList{}, &corev1.Pod{}) 761 | return s 762 | }()).WithRuntimeObjects(&corev1.Pod{ 763 | ObjectMeta: metav1.ObjectMeta{ 764 | Name: "pod-1", 765 | Labels: map[string]string{ 766 | "v1.custompodautoscaler.com/owned-by": "othercpa", 767 | }, 768 | OwnerReferences: []metav1.OwnerReference{ 769 | { 770 | Kind: "custompodautoscaler", 771 | APIVersion: "custompodautoscaler.com/v1", 772 | Name: "testcpa", 773 | }, 774 | }, 775 | }, 776 | }, 777 | &corev1.Pod{ 778 | ObjectMeta: metav1.ObjectMeta{ 779 | Name: "pod2", 780 | }, 781 | }, 782 | &corev1.Pod{ 783 | ObjectMeta: metav1.ObjectMeta{ 784 | Name: "pod-3", 785 | }, 786 | }).Build(), 787 | Scheme: &runtime.Scheme{}, 788 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 789 | return nil 790 | }, 791 | }, 792 | logger: log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 793 | instance: &custompodautoscalercomv1.CustomPodAutoscaler{ 794 | TypeMeta: metav1.TypeMeta{ 795 | Kind: "custompodautoscaler", 796 | APIVersion: "custompodautoscaler.com/v1", 797 | }, 798 | ObjectMeta: metav1.ObjectMeta{ 799 | Name: "testcpa", 800 | UID: "testuid", 801 | }, 802 | }, 803 | }, 804 | { 805 | description: "One pod found, not managed by this CPA", 806 | expectedErr: nil, 807 | reconciler: &k8sreconcile.KubernetesResourceReconciler{ 808 | Client: fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 809 | s := runtime.NewScheme() 810 | s.AddKnownTypes(schema.GroupVersion{ 811 | Group: "", 812 | Version: "v1", 813 | }, &corev1.PodList{}, &corev1.Pod{}) 814 | return s 815 | }()).WithRuntimeObjects( 816 | &corev1.Pod{ 817 | ObjectMeta: metav1.ObjectMeta{ 818 | Name: "othercpa", 819 | Labels: map[string]string{ 820 | "v1.custompodautoscaler.com/owned-by": "testcpa", 821 | }, 822 | OwnerReferences: []metav1.OwnerReference{ 823 | { 824 | Kind: "custompodautoscaler", 825 | APIVersion: "custompodautoscaler.com/v1", 826 | Name: "othercpa", 827 | }, 828 | }, 829 | }, 830 | }, 831 | ).Build(), 832 | Scheme: &runtime.Scheme{}, 833 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 834 | return nil 835 | }, 836 | }, 837 | logger: log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 838 | instance: &custompodautoscalercomv1.CustomPodAutoscaler{ 839 | TypeMeta: metav1.TypeMeta{ 840 | Kind: "custompodautoscaler", 841 | APIVersion: "custompodautoscaler.com/v1", 842 | }, 843 | ObjectMeta: metav1.ObjectMeta{ 844 | Name: "testcpa", 845 | UID: "testuid", 846 | }, 847 | }, 848 | }, 849 | { 850 | description: "One pod found, managed by CPA, using instance name, name matches", 851 | expectedErr: nil, 852 | reconciler: &k8sreconcile.KubernetesResourceReconciler{ 853 | Client: fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 854 | s := runtime.NewScheme() 855 | s.AddKnownTypes(schema.GroupVersion{ 856 | Group: "", 857 | Version: "v1", 858 | }, &corev1.PodList{}, &corev1.Pod{}) 859 | return s 860 | }()).WithRuntimeObjects( 861 | &corev1.Pod{ 862 | ObjectMeta: metav1.ObjectMeta{ 863 | Name: "testcpa", 864 | Labels: map[string]string{ 865 | "v1.custompodautoscaler.com/owned-by": "testcpa", 866 | }, 867 | OwnerReferences: []metav1.OwnerReference{ 868 | { 869 | Kind: "custompodautoscaler", 870 | APIVersion: "custompodautoscaler.com/v1", 871 | Name: "testcpa", 872 | }, 873 | }, 874 | }, 875 | }, 876 | ).Build(), 877 | Scheme: &runtime.Scheme{}, 878 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 879 | return nil 880 | }, 881 | }, 882 | logger: log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 883 | instance: &custompodautoscalercomv1.CustomPodAutoscaler{ 884 | TypeMeta: metav1.TypeMeta{ 885 | Kind: "custompodautoscaler", 886 | APIVersion: "custompodautoscaler.com/v1", 887 | }, 888 | ObjectMeta: metav1.ObjectMeta{ 889 | Name: "testcpa", 890 | UID: "testuid", 891 | }, 892 | }, 893 | }, 894 | { 895 | description: "One pod found, managed by CPA, using instance name, name doesn't match, delete fail", 896 | expectedErr: errors.New("fail to delete"), 897 | reconciler: &k8sreconcile.KubernetesResourceReconciler{ 898 | Client: func() *fakeClient { 899 | fclient := &fakeClient{} 900 | fclient.list = func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { 901 | pods := list.(*corev1.PodList) 902 | pods.Items = []corev1.Pod{ 903 | { 904 | ObjectMeta: metav1.ObjectMeta{ 905 | Name: "testcpa-mismatch", 906 | Labels: map[string]string{ 907 | "v1.custompodautoscaler.com/owned-by": "testcpa", 908 | }, 909 | OwnerReferences: []metav1.OwnerReference{ 910 | { 911 | Kind: "custompodautoscaler", 912 | APIVersion: "custompodautoscaler.com/v1", 913 | Name: "testcpa", 914 | }, 915 | }, 916 | }, 917 | }, 918 | } 919 | return nil 920 | } 921 | 922 | fclient.delete = func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { 923 | return errors.New("fail to delete") 924 | } 925 | return fclient 926 | }(), 927 | Scheme: &runtime.Scheme{}, 928 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 929 | return nil 930 | }, 931 | }, 932 | logger: log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 933 | instance: &custompodautoscalercomv1.CustomPodAutoscaler{ 934 | TypeMeta: metav1.TypeMeta{ 935 | Kind: "custompodautoscaler", 936 | APIVersion: "custompodautoscaler.com/v1", 937 | }, 938 | ObjectMeta: metav1.ObjectMeta{ 939 | Name: "testcpa", 940 | UID: "testuid", 941 | }, 942 | }, 943 | }, 944 | { 945 | description: "One pod found, managed by CPA, using instance name, name doesn't match, delete", 946 | expectedErr: nil, 947 | reconciler: &k8sreconcile.KubernetesResourceReconciler{ 948 | Client: fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 949 | s := runtime.NewScheme() 950 | s.AddKnownTypes(schema.GroupVersion{ 951 | Group: "", 952 | Version: "v1", 953 | }, &corev1.PodList{}, &corev1.Pod{}) 954 | return s 955 | }()).WithRuntimeObjects( 956 | &corev1.Pod{ 957 | ObjectMeta: metav1.ObjectMeta{ 958 | Name: "testcpa-mismatch", 959 | Labels: map[string]string{ 960 | "v1.custompodautoscaler.com/owned-by": "testcpa", 961 | }, 962 | OwnerReferences: []metav1.OwnerReference{ 963 | { 964 | Kind: "custompodautoscaler", 965 | APIVersion: "custompodautoscaler.com/v1", 966 | Name: "testcpa", 967 | }, 968 | }, 969 | }, 970 | }).Build(), 971 | Scheme: &runtime.Scheme{}, 972 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 973 | return nil 974 | }, 975 | }, 976 | logger: log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 977 | instance: &custompodautoscalercomv1.CustomPodAutoscaler{ 978 | TypeMeta: metav1.TypeMeta{ 979 | Kind: "custompodautoscaler", 980 | APIVersion: "custompodautoscaler.com/v1", 981 | }, 982 | ObjectMeta: metav1.ObjectMeta{ 983 | Name: "testcpa", 984 | UID: "testuid", 985 | }, 986 | }, 987 | }, 988 | { 989 | description: "One pod found, managed by CPA, not using instance name, name matches", 990 | expectedErr: nil, 991 | reconciler: &k8sreconcile.KubernetesResourceReconciler{ 992 | Client: fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 993 | s := runtime.NewScheme() 994 | s.AddKnownTypes(schema.GroupVersion{ 995 | Group: "", 996 | Version: "v1", 997 | }, &corev1.PodList{}, &corev1.Pod{}) 998 | return s 999 | }()).WithRuntimeObjects(&corev1.Pod{ 1000 | ObjectMeta: metav1.ObjectMeta{ 1001 | Name: "testcpa-custom", 1002 | Labels: map[string]string{ 1003 | "v1.custompodautoscaler.com/owned-by": "testcpa", 1004 | }, 1005 | OwnerReferences: []metav1.OwnerReference{ 1006 | { 1007 | Kind: "custompodautoscaler", 1008 | APIVersion: "custompodautoscaler.com/v1", 1009 | Name: "testcpa", 1010 | }, 1011 | }, 1012 | }, 1013 | }, 1014 | ).Build(), 1015 | Scheme: &runtime.Scheme{}, 1016 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 1017 | return nil 1018 | }, 1019 | }, 1020 | logger: log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 1021 | instance: &custompodautoscalercomv1.CustomPodAutoscaler{ 1022 | TypeMeta: metav1.TypeMeta{ 1023 | Kind: "custompodautoscaler", 1024 | APIVersion: "custompodautoscaler.com/v1", 1025 | }, 1026 | ObjectMeta: metav1.ObjectMeta{ 1027 | Name: "testcpa", 1028 | UID: "testuid", 1029 | }, 1030 | Spec: custompodautoscalercomv1.CustomPodAutoscalerSpec{ 1031 | Template: custompodautoscalercomv1.PodTemplateSpec{ 1032 | ObjectMeta: custompodautoscalercomv1.PodMeta{ 1033 | Name: "testcpa-custom", 1034 | }, 1035 | }, 1036 | }, 1037 | }, 1038 | }, 1039 | { 1040 | description: "One pod found, managed by CPA, not using instance name, name doesn't match, delete fail", 1041 | expectedErr: errors.New("fail to delete"), 1042 | reconciler: &k8sreconcile.KubernetesResourceReconciler{ 1043 | Client: func() *fakeClient { 1044 | fclient := &fakeClient{} 1045 | fclient.list = func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { 1046 | pods := list.(*corev1.PodList) 1047 | pods.Items = []corev1.Pod{ 1048 | { 1049 | ObjectMeta: metav1.ObjectMeta{ 1050 | Name: "testcpa-template-mismatch", 1051 | Labels: map[string]string{ 1052 | "v1.custompodautoscaler.com/owned-by": "testcpa", 1053 | }, 1054 | OwnerReferences: []metav1.OwnerReference{ 1055 | { 1056 | Kind: "custompodautoscaler", 1057 | APIVersion: "custompodautoscaler.com/v1", 1058 | Name: "testcpa", 1059 | }, 1060 | }, 1061 | }, 1062 | }, 1063 | } 1064 | return nil 1065 | } 1066 | 1067 | fclient.delete = func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { 1068 | return errors.New("fail to delete") 1069 | } 1070 | return fclient 1071 | }(), 1072 | Scheme: &runtime.Scheme{}, 1073 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 1074 | return nil 1075 | }, 1076 | }, 1077 | logger: log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 1078 | instance: &custompodautoscalercomv1.CustomPodAutoscaler{ 1079 | TypeMeta: metav1.TypeMeta{ 1080 | Kind: "custompodautoscaler", 1081 | APIVersion: "custompodautoscaler.com/v1", 1082 | }, 1083 | ObjectMeta: metav1.ObjectMeta{ 1084 | Name: "testcpa", 1085 | UID: "testuid", 1086 | }, 1087 | Spec: custompodautoscalercomv1.CustomPodAutoscalerSpec{ 1088 | Template: custompodautoscalercomv1.PodTemplateSpec{ 1089 | ObjectMeta: custompodautoscalercomv1.PodMeta{ 1090 | Name: "testcpa-template", 1091 | }, 1092 | }, 1093 | }, 1094 | }, 1095 | }, 1096 | { 1097 | description: "One pod found, managed by CPA, not using instance name, name doesn't match, delete", 1098 | expectedErr: nil, 1099 | reconciler: &k8sreconcile.KubernetesResourceReconciler{ 1100 | Client: fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 1101 | s := runtime.NewScheme() 1102 | s.AddKnownTypes(schema.GroupVersion{ 1103 | Group: "", 1104 | Version: "v1", 1105 | }, &corev1.PodList{}, &corev1.Pod{}) 1106 | return s 1107 | }()).WithRuntimeObjects( 1108 | &corev1.Pod{ 1109 | ObjectMeta: metav1.ObjectMeta{ 1110 | Name: "testcpa-template-mismatch", 1111 | Labels: map[string]string{ 1112 | "v1.custompodautoscaler.com/owned-by": "testcpa", 1113 | }, 1114 | OwnerReferences: []metav1.OwnerReference{ 1115 | { 1116 | Kind: "custompodautoscaler", 1117 | APIVersion: "custompodautoscaler.com/v1", 1118 | Name: "testcpa", 1119 | }, 1120 | }, 1121 | }, 1122 | }).Build(), 1123 | Scheme: &runtime.Scheme{}, 1124 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 1125 | return nil 1126 | }, 1127 | }, 1128 | logger: log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 1129 | instance: &custompodautoscalercomv1.CustomPodAutoscaler{ 1130 | TypeMeta: metav1.TypeMeta{ 1131 | Kind: "custompodautoscaler", 1132 | APIVersion: "custompodautoscaler.com/v1", 1133 | }, 1134 | ObjectMeta: metav1.ObjectMeta{ 1135 | Name: "testcpa", 1136 | UID: "testuid", 1137 | }, 1138 | Spec: custompodautoscalercomv1.CustomPodAutoscalerSpec{ 1139 | Template: custompodautoscalercomv1.PodTemplateSpec{ 1140 | ObjectMeta: custompodautoscalercomv1.PodMeta{ 1141 | Name: "testcpa-template", 1142 | }, 1143 | }, 1144 | }, 1145 | }, 1146 | }, 1147 | { 1148 | description: "Three pods found, one managed by CPA, not using instance name, name doesn't match, delete", 1149 | expectedErr: nil, 1150 | reconciler: &k8sreconcile.KubernetesResourceReconciler{ 1151 | Client: fake.NewClientBuilder().WithScheme(func() *runtime.Scheme { 1152 | s := runtime.NewScheme() 1153 | s.AddKnownTypes(schema.GroupVersion{ 1154 | Group: "", 1155 | Version: "v1", 1156 | }, &corev1.PodList{}, &corev1.Pod{}) 1157 | return s 1158 | }()).WithRuntimeObjects( 1159 | &corev1.Pod{ 1160 | ObjectMeta: metav1.ObjectMeta{ 1161 | Name: "testcpa-template-mismatch", 1162 | Labels: map[string]string{ 1163 | "v1.custompodautoscaler.com/owned-by": "testcpa", 1164 | }, 1165 | OwnerReferences: []metav1.OwnerReference{ 1166 | { 1167 | Kind: "custompodautoscaler", 1168 | APIVersion: "custompodautoscaler.com/v1", 1169 | Name: "testcpa", 1170 | }, 1171 | }, 1172 | }, 1173 | }, 1174 | &corev1.Pod{ 1175 | ObjectMeta: metav1.ObjectMeta{ 1176 | Name: "othercpa", 1177 | Labels: map[string]string{ 1178 | "v1.custompodautoscaler.com/owned-by": "othercpa", 1179 | }, 1180 | OwnerReferences: []metav1.OwnerReference{ 1181 | { 1182 | Kind: "custompodautoscaler", 1183 | APIVersion: "custompodautoscaler.com/v1", 1184 | Name: "othercpa", 1185 | }, 1186 | }, 1187 | }, 1188 | }, 1189 | &corev1.Pod{ 1190 | ObjectMeta: metav1.ObjectMeta{ 1191 | Name: "different-pod", 1192 | }, 1193 | }, 1194 | ).Build(), 1195 | Scheme: &runtime.Scheme{}, 1196 | ControllerReferencer: func(owner, object metav1.Object, scheme *runtime.Scheme) error { 1197 | return nil 1198 | }, 1199 | }, 1200 | logger: log.WithValues("Request.Namespace", "test", "Request.Name", "test"), 1201 | instance: &custompodautoscalercomv1.CustomPodAutoscaler{ 1202 | TypeMeta: metav1.TypeMeta{ 1203 | Kind: "custompodautoscaler", 1204 | APIVersion: "custompodautoscaler.com/v1", 1205 | }, 1206 | ObjectMeta: metav1.ObjectMeta{ 1207 | Name: "testcpa", 1208 | UID: "testuid", 1209 | }, 1210 | Spec: custompodautoscalercomv1.CustomPodAutoscalerSpec{ 1211 | Template: custompodautoscalercomv1.PodTemplateSpec{ 1212 | ObjectMeta: custompodautoscalercomv1.PodMeta{ 1213 | Name: "testcpa-template", 1214 | }, 1215 | }, 1216 | }, 1217 | }, 1218 | }, 1219 | } 1220 | for _, test := range tests { 1221 | t.Run(test.description, func(t *testing.T) { 1222 | err := test.reconciler.PodCleanup(test.logger, test.instance) 1223 | if !cmp.Equal(err, test.expectedErr, equateErrorMessage) { 1224 | t.Errorf("error mismatch (-want +got):\n%s", cmp.Diff(test.expectedErr, err, equateErrorMessage)) 1225 | return 1226 | } 1227 | }) 1228 | } 1229 | } 1230 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package main 4 | 5 | import _ "honnef.co/go/tools/cmd/staticcheck" 6 | --------------------------------------------------------------------------------