├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── generate.yaml │ ├── goreleaser.yaml │ ├── main.yaml │ └── publish-image.yaml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── apis └── autoscaling │ └── v1 │ ├── contains.go │ ├── contains_test.go │ ├── contains_weekly_test.go │ ├── groupversion_info.go │ ├── schedule_types.go │ ├── schedule_types_test.go │ ├── scheduledpodautoscaler_types.go │ └── zz_generated.deepcopy.go ├── config ├── certmanager │ ├── certificate.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── crd │ ├── bases │ │ ├── autoscaling.d-kuro.github.io_scheduledpodautoscalers.yaml │ │ └── autoscaling.d-kuro.github.io_schedules.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_scheduledpodautoscalers.yaml │ │ ├── cainjection_in_schedules.yaml │ │ ├── webhook_in_scheduledpodautoscalers.yaml │ │ └── webhook_in_schedules.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ ├── manager_webhook_patch.yaml │ └── webhookcainjection_patch.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ ├── role_binding.yaml │ ├── schedule_editor_role.yaml │ ├── schedule_viewer_role.yaml │ ├── scheduledpodautoscaler_editor_role.yaml │ └── scheduledpodautoscaler_viewer_role.yaml ├── samples │ ├── autoscaling_v1_schedule.yaml │ └── autoscaling_v1_scheduledpodautoscaler.yaml └── webhook │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ ├── manifests.yaml │ └── service.yaml ├── controllers └── autoscaling │ ├── internal │ └── testutil │ │ └── testutil.go │ ├── metrics.go │ ├── schedule_controller.go │ ├── schedule_controller_test.go │ ├── scheduledpodautoscaler_controller.go │ ├── scheduledpodautoscaler_controller_test.go │ └── suite_test.go ├── go.mod ├── go.sum ├── hack ├── boilerplate.go.txt └── bump-version.sh ├── main.go └── manifests ├── crd ├── autoscaling.d-kuro.github.io_scheduledpodautoscalers.yaml ├── autoscaling.d-kuro.github.io_schedules.yaml ├── kustomization.yaml └── legacy │ ├── autoscaling.d-kuro.github.io_scheduledpodautoscalers.yaml │ ├── autoscaling.d-kuro.github.io_schedules.yaml │ └── kustomization.yaml ├── deployment ├── kustomization.yaml └── scheduled-pod-autoscaler.yaml ├── install ├── install.yaml ├── kustomization.yaml └── legacy │ ├── install.yaml │ └── kustomization.yaml └── rbac ├── kustomization.yaml ├── leader_election_role.yaml ├── leader_election_role_binding.yaml ├── role.yaml ├── role_binding.yaml └── service_account.yaml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | reviewers: 8 | - d-kuro 9 | - package-ecosystem: gomod 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | reviewers: 14 | - d-kuro 15 | ignore: 16 | - dependency-name: "k8s.io/api" 17 | - dependency-name: "k8s.io/apimachinery" 18 | - dependency-name: "k8s.io/client-go" 19 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # ******** NOTE ******** 12 | 13 | name: "CodeQL" 14 | 15 | on: 16 | push: 17 | branches: [ main ] 18 | paths-ignore: ['**.md'] 19 | pull_request: 20 | # The branches below must be a subset of the branches above 21 | branches: [ main ] 22 | paths-ignore: ['**.md'] 23 | schedule: 24 | - cron: '16 20 * * 6' 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | runs-on: ubuntu-latest 30 | 31 | strategy: 32 | fail-fast: false 33 | matrix: 34 | language: [ 'go' ] 35 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 36 | # Learn more... 37 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.github/workflows/generate.yaml: -------------------------------------------------------------------------------- 1 | name: generate 2 | on: 3 | pull_request: 4 | types: [opened, synchronize] 5 | paths-ignore: ['**.md'] 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-go@v2 12 | with: 13 | go-version: 1.16 14 | - uses: actions/cache@v2.1.6 15 | with: 16 | path: ~/go/pkg/mod 17 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 18 | restore-keys: | 19 | ${{ runner.os }}-go- 20 | # Since the output may change depending on the version of kustomize. 21 | # Install kustomize with a pinned version instead of pre-installed on GitHub Actions. 22 | - name: Install kustomize 23 | run: | 24 | version='v4.0.5' 25 | os=$(go env GOOS) 26 | arch=$(go env GOARCH) 27 | 28 | curl -L https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2F${version}/kustomize_${version}_${os}_${arch}.tar.gz | tar -xz -C /tmp/ 29 | sudo mv /tmp/kustomize /usr/local/bin/kustomize 30 | - name: Check generated files up to date 31 | run: make check-generated-files-up-to-date 32 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yaml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | fetch-depth: 0 13 | - uses: actions/setup-go@v2 14 | with: 15 | go-version: 1.16 16 | - uses: goreleaser/goreleaser-action@v2 17 | with: 18 | version: latest 19 | args: release --rm-dist 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | push: 4 | branches: [main] 5 | tags: ["v*"] 6 | paths-ignore: ['**.md'] 7 | pull_request: 8 | types: [opened, synchronize] 9 | paths-ignore: ['**.md'] 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-go@v2 16 | with: 17 | go-version: 1.16 18 | - uses: actions/cache@v2.1.6 19 | with: 20 | path: ~/go/pkg/mod 21 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 22 | restore-keys: | 23 | ${{ runner.os }}-go- 24 | - name: Install tools 25 | run: make install-tools 26 | - name: Install kubebuilder 27 | run: | 28 | # ref: https://book.kubebuilder.io/quick-start.html#installation 29 | os=$(go env GOOS) 30 | arch=$(go env GOARCH) 31 | 32 | # download kubebuilder and extract it to tmp 33 | curl -L https://go.kubebuilder.io/dl/2.3.1/${os}/${arch} | tar -xz -C /tmp/ 34 | 35 | # move to a long-term location and put it on your path 36 | # (you'll need to set the KUBEBUILDER_ASSETS env var if you put it somewhere else) 37 | sudo mv /tmp/kubebuilder_2.3.1_${os}_${arch} /usr/local/kubebuilder 38 | # ref: https://text.superbrothers.dev/200510-ensure-go-mod-is-already-tidied-on-ci/ 39 | - name: Ensure go.mod is already tidied 40 | run: go mod tidy && git diff -s --exit-code go.sum 41 | - uses: golangci/golangci-lint-action@v2.5.2 42 | with: 43 | version: v1.38 44 | args: --timeout=3m0s 45 | - name: Run tests 46 | run: make test 47 | -------------------------------------------------------------------------------- /.github/workflows/publish-image.yaml: -------------------------------------------------------------------------------- 1 | name: publish-image 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - id: docker_meta 12 | uses: crazy-max/ghaction-docker-meta@v1 13 | with: 14 | images: d-kuro/scheduled-pod-autoscaler 15 | tag-latest: false 16 | - uses: docker/setup-qemu-action@v1 17 | - uses: docker/setup-buildx-action@v1.3.0 18 | with: 19 | version: latest 20 | - uses: docker/login-action@v1.9.0 21 | with: 22 | registry: ghcr.io 23 | username: ${{ github.repository_owner }} 24 | password: ${{ secrets.CR_PAT }} 25 | - uses: docker/build-push-action@v2.5.0 26 | with: 27 | platforms: linux/amd64, linux/arm64 28 | push: true 29 | tags: ghcr.io/${{ steps.docker_meta.outputs.tags }} 30 | labels: ${{ steps.docker_meta.outputs.labels }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Build binary 18 | bin/ 19 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: true 3 | linters: 4 | enable-all: true 5 | disable: 6 | - dupl 7 | - cyclop 8 | - exhaustivestruct 9 | - funlen 10 | - gochecknoglobals 11 | - gochecknoinits 12 | - goerr113 13 | - gomnd 14 | - lll 15 | - maligned 16 | - paralleltest 17 | - testpackage 18 | - wrapcheck 19 | - wsl 20 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - main: ./main.go 3 | binary: manager 4 | goos: 5 | - windows 6 | - darwin 7 | - linux 8 | goarch: 9 | - amd64 10 | - arm64 11 | env: 12 | - CGO_ENABLED=0 13 | - GO111MODULE=on 14 | checksum: 15 | name_template: checksums.txt 16 | archives: 17 | - format: tar.gz 18 | changelog: 19 | filters: 20 | exclude: 21 | - "Merge pull request" 22 | - "Merge branch" 23 | release: 24 | github: 25 | owner: d-kuro 26 | name: scheduled-pod-autoscaler 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.15 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go mod download 11 | 12 | # Copy the go source 13 | COPY main.go main.go 14 | COPY apis/ apis/ 15 | COPY controllers/ controllers/ 16 | 17 | # Build 18 | RUN CGO_ENABLED=0 GO111MODULE=on go build -a -o manager main.go 19 | 20 | # Use distroless as minimal base image to package the manager binary 21 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 22 | FROM gcr.io/distroless/static:nonroot 23 | WORKDIR / 24 | COPY --from=builder /workspace/manager . 25 | USER nonroot:nonroot 26 | 27 | ENTRYPOINT ["/manager"] 28 | -------------------------------------------------------------------------------- /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. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Image URL to use all building/pushing image targets 3 | IMG ?= controller:latest 4 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 5 | CRD_OPTIONS ?= "crd:trivialVersions=true" 6 | 7 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 8 | ifeq (,$(shell go env GOBIN)) 9 | GOBIN=$(shell go env GOPATH)/bin 10 | else 11 | GOBIN=$(shell go env GOBIN) 12 | endif 13 | 14 | all: manager 15 | 16 | # Run tests 17 | test: generate fmt vet manifests 18 | go test ./... -coverprofile cover.out --race 19 | 20 | # Build manager binary 21 | manager: generate fmt vet 22 | go build -o bin/manager main.go 23 | 24 | # Run against the configured Kubernetes cluster in ~/.kube/config 25 | run: generate fmt vet manifests 26 | go run ./main.go 27 | 28 | # Install CRDs into a cluster 29 | install: manifests 30 | kustomize build config/crd | kubectl apply -f - 31 | 32 | # Uninstall CRDs from a cluster 33 | uninstall: manifests 34 | kustomize build config/crd | kubectl delete -f - 35 | 36 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 37 | deploy: manifests 38 | cd config/manager && kustomize edit set image controller=${IMG} 39 | kustomize build config/default | kubectl apply -f - 40 | 41 | # Generate manifests e.g. CRD, RBAC etc. 42 | manifests: controller-gen 43 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases 44 | 45 | # Run go fmt against code 46 | fmt: 47 | go fmt ./... 48 | 49 | # Run go vet against code 50 | vet: 51 | go vet ./... 52 | 53 | # Generate code 54 | generate: controller-gen 55 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 56 | 57 | # Build the docker image 58 | docker-build: test 59 | docker build . -t ${IMG} 60 | 61 | # Push the docker image 62 | docker-push: 63 | docker push ${IMG} 64 | 65 | # find or download controller-gen 66 | # download controller-gen if necessary 67 | controller-gen: 68 | ifeq (, $(shell which controller-gen)) 69 | @{ \ 70 | set -e ;\ 71 | CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\ 72 | cd $$CONTROLLER_GEN_TMP_DIR ;\ 73 | go mod init tmp ;\ 74 | go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.4.1;\ 75 | rm -rf $$CONTROLLER_GEN_TMP_DIR ;\ 76 | } 77 | CONTROLLER_GEN=$(GOBIN)/controller-gen 78 | else 79 | CONTROLLER_GEN=$(shell which controller-gen) 80 | endif 81 | 82 | # find or download controller-gen v0.3.0 83 | # used generate CRD for Kubernetes < v1.16 84 | controller-gen-v3: 85 | ifeq (, $(shell which controller-gen-v3)) 86 | @{ \ 87 | set -e ;\ 88 | CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\ 89 | git -c advice.detachedHead=false clone --single-branch -b v0.3.0 \ 90 | https://github.com/kubernetes-sigs/controller-tools.git $$CONTROLLER_GEN_TMP_DIR ;\ 91 | cd $$CONTROLLER_GEN_TMP_DIR ;\ 92 | go build -o $(GOBIN)/controller-gen-v3 ./cmd/controller-gen/... ;\ 93 | rm -rf $$CONTROLLER_GEN_TMP_DIR ;\ 94 | } 95 | CONTROLLER_GEN_V3=$(GOBIN)/controller-gen-v3 96 | else 97 | CONTROLLER_GEN_V3=$(shell which controller-gen-v3) 98 | endif 99 | 100 | # install tools 101 | install-tools: controller-gen controller-gen-v3 102 | 103 | # generate all 104 | generate-all: generate manifests generate-install generate-install-legacy 105 | 106 | # generate install manifests 107 | generate-install: controller-gen 108 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=scheduled-pod-autoscaler-role paths="./..." \ 109 | output:crd:artifacts:config=manifests/crd \ 110 | output:rbac:artifacts:config=manifests/rbac 111 | kustomize build ./manifests/install/ > ./manifests/install/install.yaml 112 | 113 | # generate install manifests (Kubernetes < v1.16) 114 | generate-install-legacy: controller-gen-v3 115 | $(CONTROLLER_GEN_V3) $(CRD_OPTIONS) paths="./..." output:crd:artifacts:config=manifests/crd/legacy 116 | kustomize build ./manifests/install/legacy/ > ./manifests/install/legacy/install.yaml 117 | 118 | # check generated files up to date 119 | # If this fails, try "make generate-all" 120 | check-generated-files-up-to-date: generate-all 121 | git diff --exit-code 122 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: d-kuro.github.io 2 | multigroup: true 3 | repo: github.com/d-kuro/scheduled-pod-autoscaler 4 | resources: 5 | - group: autoscaling 6 | kind: ScheduledPodAutoscaler 7 | version: v1 8 | - group: autoscaling 9 | kind: Schedule 10 | version: v1 11 | version: "2" 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scheduled-pod-autoscaler 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/d-kuro/scheduled-pod-autoscaler)](https://pkg.go.dev/github.com/d-kuro/scheduled-pod-autoscaler) ![](https://github.com/d-kuro/scheduled-pod-autoscaler/workflows/main/badge.svg) 4 | 5 | Custom Kubernetes controller for GitOps native scheduled scaling. 6 | 7 | Autoscaling with `HorizontalPodAutoscaler` is difficult to use for spike access etc. 8 | `ScheduledPodAutoscaler` can for more flexible autoscaling by controlling min/max replicas at specific times. 9 | 10 | ## Overview 11 | 12 | `ScheduledPodAutoscaler` is made up of two custom resources. 13 | 14 | The parent-child relationship can look like this: 15 | 16 | ```console 17 | $ kubectl tree scheduledpodautoscaler nginx 18 | NAMESPACE NAME READY REASON AGE 19 | default ScheduledPodAutoscaler/nginx - 6m5s 20 | default ├─HorizontalPodAutoscaler/nginx - 6m5s 21 | default ├─Schedule/test-1 - 6m4s 22 | default ├─Schedule/test-2 - 6m4s 23 | default └─Schedule/test-3 - 6m4s 24 | ``` 25 | 26 | ### ScheduledPodAutoscaler 27 | 28 | `ScheduledPodAutoscaler` is a custom resource that wraps `HorizontalPodAutoscaler`. 29 | The `ScheduledPodAutoscaler` Controller generates a `HorizontalPodAutoscaler` from this resource. 30 | 31 | The specs of the `HorizontalPodAutoscaler` defined here will be used when no scheduled scaling is taking place. 32 | 33 | for example: 34 | 35 | ```yaml 36 | apiVersion: autoscaling.d-kuro.github.io/v1 37 | kind: ScheduledPodAutoscaler 38 | metadata: 39 | name: nginx 40 | spec: 41 | horizontalPodAutoscalerSpec: 42 | scaleTargetRef: 43 | apiVersion: apps/v1 44 | kind: Deployment 45 | name: nginx 46 | minReplicas: 3 47 | maxReplicas: 10 48 | metrics: 49 | - type: Resource 50 | resource: 51 | name: cpu 52 | target: 53 | type: Utilization 54 | averageUtilization: 70 55 | ``` 56 | 57 | ```console 58 | $ kubectl get spa # You can use spa as a short name of scheduledpodautoscaler. 59 | NAME MINPODS MAXPODS STATUS AGE 60 | nginx 3 10 Available 6m52s 61 | ``` 62 | 63 | ### Schedule 64 | 65 | `Schedule` is a custom resource for defining scheduled scaling. 66 | You can define multiple children's `Schedule` for the parent `ScheduledPodAutoscaler`. 67 | 68 | The `ScheduledPodAutoscaler` controller refers to the `Schedule` and 69 | rewrites `HorizontalPodAutoscaler` created by `ScheduledPodAutoscaler` when it is time for scheduled scaling. 70 | `HorizontalPodAutoscaler` is not managed in Git, so there are no diffs in GitOps. 71 | 72 | > 📝 Note: A case of schedule conflicts 73 | > 74 | > In case of a schedule conflict, using the maximum value of min/max replicas. 75 | 76 | > 📝 Note: Warm-up time 77 | > 78 | > The `ScheduledPodAutoscaler` controller only changes the min/max replica of `HorizontalPodAutoscaler`. 79 | > Launching the Pod will take some time. 80 | > Be sure to set a generous amount of time for scheduled scaling. 81 | 82 | ```console 83 | $ kubectl get schedule -o wide 84 | NAME REFERENCE TYPE STARTTIME ENDTIME STARTDAYOFWEEK ENDDAYOFWEEK MINPODS MAXPODS STATUS AGE 85 | test-1 nginx Weekly 20:10 20:15 Saturday Saturday 1 1 Available 4m49s 86 | test-2 nginx Daily 20:20 20:25 2 2 Available 4m49s 87 | test-3 nginx OneShot 2020-10-31T20:30 2020-10-31T20:35 4 4 Completed 4m49s 88 | ``` 89 | 90 | `Schedule` supports 3 different schedule types. 91 | 92 | #### type: Weekly 93 | 94 | Write the time in the format of `HH:mm` and specify the day of the week. 95 | 96 | ```yaml 97 | apiVersion: autoscaling.d-kuro.github.io/v1 98 | kind: Schedule 99 | metadata: 100 | name: nginx-push-notification 101 | spec: 102 | scaleTargetRef: 103 | apiVersion: autoscaling.d-kuro.github.io/v1 104 | kind: ScheduledPodAutoscaler 105 | name: nginx 106 | minReplicas: 10 107 | maxReplicas: 20 108 | type: Weekly 109 | startDayOfWeek: Monday 110 | startTime: "11:50" 111 | endDayOfWeek: Wednesday 112 | endTime: "13:00" 113 | timeZone: Asia/Tokyo 114 | ``` 115 | 116 | #### type: Daily 117 | 118 | Write the time in the format of `HH:mm`. 119 | 120 | ```yaml 121 | apiVersion: autoscaling.d-kuro.github.io/v1 122 | kind: Schedule 123 | metadata: 124 | name: nginx-push-notification 125 | spec: 126 | scaleTargetRef: 127 | apiVersion: autoscaling.d-kuro.github.io/v1 128 | kind: ScheduledPodAutoscaler 129 | name: nginx 130 | minReplicas: 10 131 | maxReplicas: 20 132 | type: Daily 133 | startTime: "11:50" 134 | endTime: "13:00" 135 | timeZone: Asia/Tokyo 136 | ``` 137 | 138 | #### type: OneShot 139 | 140 | Write the time in the format of `yyyy-MM-ddTHH:mm`. 141 | 142 | ```yaml 143 | apiVersion: autoscaling.d-kuro.github.io/v1 144 | kind: Schedule 145 | metadata: 146 | name: nginx-push-notification 147 | spec: 148 | scaleTargetRef: 149 | apiVersion: autoscaling.d-kuro.github.io/v1 150 | kind: ScheduledPodAutoscaler 151 | name: nginx 152 | minReplicas: 10 153 | maxReplicas: 20 154 | type: OneShot 155 | startTime: "2020-09-01T10:00" 156 | endTime: "2020-09-10T19:00" 157 | timeZone: Asia/Tokyo 158 | ``` 159 | 160 | ## Install 161 | 162 | All resources (the CRDs, Deployment and RBAC) 163 | are included in a single YAML manifest file. 164 | 165 | By default, it is installed in the `kube-system` namespace. 166 | 167 | ```console 168 | # Kubernetes v1.16+ 169 | $ kubectl apply -f https://raw.githubusercontent.com/d-kuro/scheduled-pod-autoscaler/v0.0.3/manifests/install/install.yaml 170 | 171 | # Kubernetes < v1.16 172 | $ kubectl apply -f https://raw.githubusercontent.com/d-kuro/scheduled-pod-autoscaler/v0.0.3/manifests/install/legacy/install.yaml 173 | ``` 174 | 175 | ### Kustomize 176 | 177 | Support for kustomize remote base: 178 | 179 | ```yaml 180 | apiVersion: kustomize.config.k8s.io/v1beta1 181 | kind: Kustomization 182 | 183 | resources: 184 | # Kubernetes v1.16+ 185 | - github.com/d-kuro/scheduled-pod-autoscaler/manifests/install?ref=v0.0.3 186 | 187 | # Kubernetes < v1.16 188 | # - github.com/d-kuro/scheduled-pod-autoscaler/manifests/install/legacy?ref=v0.0.3 189 | ``` 190 | 191 | ## Spec 192 | 193 | ### ScheduledPodAutoscaler 194 | 195 | | name | type | required | description | 196 | | - | - | - | - | 197 | | `.spec.horizontalPodAutoscalerSpec` | `Object` | required | HorizontalPodAutoscalerSpec is HorizontalPodAutoscaler v2beta2 API spec. ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#horizontalpodautoscaler-v2beta2-autoscaling | 198 | 199 | ### Schedule 200 | 201 | | name | type | required | description | 202 | | - | - | - | - | 203 | | `.spec.scaleTargetRef` | `Object` | required | ScaleTargetRef points to the target resource to scale, and is used to the pods for which metrics should be collected, as well as to actually change the replica count. | 204 | | `.spec.scaleTargetRef.apiVersion` | `string` | optional | API version of the referent. | 205 | | `.spec.scaleTargetRef.kind` | `string` | required | Kind of the referent. | 206 | | `.spec.scaleTargetRef.name` | `string` | required | Name of the referent. | 207 | | `.spec.description` | `string` | optional | Description is schedule description. | 208 | | `.spec.suspend` | `boolean` | optional | Suspend indicates whether to suspend this schedule. | 209 | | `.spec.timeZone` | `string` | optional | TimeZone is the name of the timezone used in the argument of the time.LoadLocation(name string) function. StartTime and EndTime are interpreted as the time in the time zone specified by TimeZone. If not specified, the time will be interpreted as UTC. | 210 | | `.spec.minReplicas` | `integer` | optional | MinReplicas is the lower limit for the number of replicas to which the autoscaler can scale down. It defaults to 1 pod. | 211 | | `.spec.maxReplicas` | `integer` | optional | MaxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up. | 212 | | `.spec.type` | `string` | required | ScheduleType is a type of schedule represented by "Weekly","Daily","OneShot". | 213 | | `.spec.startDayOfWeek` | `string` | optional | StartDayOfWeek is scaling start day of week. Represented by "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday". | 214 | | `.spec.endDayOfWeek` | `string` | optional | EndDayOfWeek is scaling end day of week. Represented by "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday". | 215 | | `.spec.startTime` | `string` | required | StartTime is scaling start time. Defined in RFC3339 based format. Different formats are evaluated depending on ScheduleType. e.g. OneShot(yyyy-MM-ddTHH:mm), Weekly(HH:mm), Daily(HH:mm) | 216 | | `.spec.endTime` | `string` | required | EndTime is scaling end time. Defined in RFC3339 based format. Different formats are evaluated depending on ScheduleType. e.g. OneShot(yyyy-MM-ddTHH:mm), Weekly(HH:mm), Daily(HH:mm) | 217 | 218 | ## Metrics 219 | 220 | scheduled-pod-autoscaler exports metrics in [OpenMetrics](https://openmetrics.io/) format. 221 | 222 | You can get the metrics by access to `http://localhost:8080/metrics`. 223 | The port of the metrics endpoint can be changed using the `metrics-addr` option. 224 | 225 | ### Custom Metrics 226 | 227 | | name | type | description | 228 | | - | - | - | 229 | | `scheduled_pod_auroscaler_min_replicas` | `gauge` | Lower limit for the number of pods that can be set by the scheduled pod autoscaler | 230 | | `scheduled_pod_auroscaler_max_replicas` | `gauge` | Upper limit for the number of pods that can be set by the scheduled pod autoscaler | 231 | 232 | ## Controller Options 233 | 234 | | name | type | description | 235 | | - | - | - | 236 | | `--enable-leader-election` | `bool` | Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager. | 237 | | `--metrics-addr` | `string` | The address the metric endpoint binds to. (default ":8080") | 238 | | `--probe-addr` | `string` | The address the liveness probe and readiness probe endpoints bind to. (default ":9090") | 239 | | `--zap-devel` | `bool` | Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error) | 240 | | `--zap-encoder` | `value` | Zap log encoding ('json' or 'console') | 241 | | `--zap-log-level` | `value` | Zap Level to configure the verbosity of logging. Can be one of 'debug', 'info', 'error', or any integer value > 0 which corresponds to custom debug levels of increasing verbosity | 242 | | `--zap-stacktrace-level` | `value` | Zap Level at and above which stacktraces are captured (one of 'info', 'error'). | 243 | | `--kubeconfig` | `string` | Paths to a kubeconfig. Only required if out-of-cluster. | 244 | | `--master` | `string` | (Deprecated: switch to --kubeconfig) The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster. | 245 | -------------------------------------------------------------------------------- /apis/autoscaling/v1/contains.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | // embed tzdata. 8 | _ "time/tzdata" 9 | ) 10 | 11 | var weekdays = map[string]time.Weekday{ 12 | "Sunday": time.Weekday(0), 13 | "Monday": time.Weekday(1), 14 | "Tuesday": time.Weekday(2), 15 | "Wednesday": time.Weekday(3), 16 | "Thursday": time.Weekday(4), 17 | "Friday": time.Weekday(5), 18 | "Saturday": time.Weekday(6), 19 | } 20 | 21 | func (s *ScheduleSpec) Contains(now time.Time) (bool, error) { 22 | location, err := time.LoadLocation(s.TimeZone) 23 | if err != nil { 24 | return false, fmt.Errorf("failed to load location %s: %w", s.TimeZone, err) 25 | } 26 | 27 | now = now.In(location) 28 | 29 | switch s.ScheduleType { 30 | case Daily: 31 | return s.containsDaily(now, location) 32 | case Weekly: 33 | return s.containsWeekly(now, location) 34 | case OneShot: 35 | return s.containsOneShot(now, location) 36 | default: 37 | return false, fmt.Errorf("unsupported schedule types: %s", s.ScheduleType) 38 | } 39 | } 40 | 41 | func (s *ScheduleSpec) containsDaily(now time.Time, location *time.Location) (bool, error) { 42 | startTime, endTime, err := s.normalizeTime(now, location) 43 | if err != nil { 44 | return false, err 45 | } 46 | 47 | // true if now is [startTime, endTime) 48 | return (now.Equal(startTime) || now.After(startTime)) && now.Before(endTime), nil 49 | } 50 | 51 | func (s *ScheduleSpec) containsWeekly(now time.Time, location *time.Location) (bool, error) { 52 | startTime, endTime, err := s.normalizeTime(now, location) 53 | if err != nil { 54 | return false, err 55 | } 56 | 57 | weekdayToday, startWeekDay, endWeekDay, err := s.normalizeWeekday(startTime) 58 | if err != nil { 59 | return false, err 60 | } 61 | 62 | if startWeekDay <= weekdayToday && weekdayToday <= endWeekDay { 63 | // true if now is [startTime, endTime) 64 | return (now.Equal(startTime) || now.After(startTime)) && now.Before(endTime), nil 65 | } 66 | 67 | return false, nil 68 | } 69 | 70 | func (s *ScheduleSpec) containsOneShot(now time.Time, location *time.Location) (bool, error) { 71 | startTime, err := time.ParseInLocation("2006-01-02T15:04", s.StartTime, location) 72 | if err != nil { 73 | return false, fmt.Errorf("startTime cannot be parsed: %w", err) 74 | } 75 | 76 | endTime, err := time.ParseInLocation("2006-01-02T15:04", s.EndTime, location) 77 | if err != nil { 78 | return false, fmt.Errorf("endTime cannot be parsed: %w", err) 79 | } 80 | 81 | // true if now is [startTime, endTime) 82 | return (now.Equal(startTime) || now.After(startTime)) && now.Before(endTime), nil 83 | } 84 | 85 | func (s *ScheduleSpec) normalizeTime(now time.Time, location *time.Location) (normalizedStartTime time.Time, normalizedEndTime time.Time, err error) { 86 | startTime, err := time.ParseInLocation("15:04", s.StartTime, location) 87 | if err != nil { 88 | return time.Time{}, time.Time{}, fmt.Errorf("startTime cannot be parsed: %w", err) 89 | } 90 | 91 | endTime, err := time.ParseInLocation("15:04", s.EndTime, location) 92 | if err != nil { 93 | return time.Time{}, time.Time{}, fmt.Errorf("endTime cannot be parsed: %w", err) 94 | } 95 | 96 | normalizedStartTime = time.Date( 97 | now.Year(), now.Month(), now.Day(), 98 | startTime.Hour(), startTime.Minute(), 0, 0, location) 99 | 100 | normalizedEndTime = time.Date( 101 | now.Year(), now.Month(), now.Day(), 102 | endTime.Hour(), endTime.Minute(), 0, 0, location) 103 | 104 | if normalizedEndTime.Before(normalizedStartTime) { 105 | if now.Before(normalizedStartTime) { 106 | normalizedStartTime = normalizedStartTime.AddDate(0, 0, -1) 107 | } else { 108 | normalizedEndTime = normalizedEndTime.AddDate(0, 0, 1) 109 | } 110 | } 111 | 112 | return normalizedStartTime, normalizedEndTime, nil 113 | } 114 | 115 | func (s *ScheduleSpec) normalizeWeekday(startTime time.Time) ( 116 | weekdayToday time.Weekday, startWeekDay time.Weekday, endWeekDay time.Weekday, err error) { 117 | startWeekDay, found := weekdays[s.StartDayOfWeek] 118 | if !found { 119 | return 0, 0, 0, fmt.Errorf("start-day-of-week %s is not found", s.StartDayOfWeek) 120 | } 121 | 122 | endWeekDay, found = weekdays[s.EndDayOfWeek] 123 | if !found { 124 | return 0, 0, 0, fmt.Errorf("end-day-of-week %s is invalid", s.EndDayOfWeek) 125 | } 126 | 127 | weekdayToday = startTime.Weekday() 128 | if startWeekDay > endWeekDay { 129 | // normalize weekday 130 | endWeekDay = 7 - startWeekDay + endWeekDay 131 | weekdayToday = (7 + weekdayToday - startWeekDay) % 7 132 | startWeekDay = 0 133 | } 134 | 135 | return weekdayToday, startWeekDay, endWeekDay, nil 136 | } 137 | -------------------------------------------------------------------------------- /apis/autoscaling/v1/contains_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestScheduleSpecContainsDaily(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | spec ScheduleSpec 12 | now time.Time 13 | expected bool 14 | }{ 15 | { 16 | name: "case[1]", 17 | spec: ScheduleSpec{ScheduleType: Daily, StartTime: "10:00", EndTime: "19:00"}, 18 | now: time.Date(2018, 9, 2, 11, 00, 0, 0, time.UTC), 19 | expected: true, 20 | }, 21 | { 22 | name: "case[2]", 23 | spec: ScheduleSpec{ScheduleType: Daily, StartTime: "10:00", EndTime: "19:00"}, 24 | now: time.Date(2018, 9, 2, 20, 00, 0, 0, time.UTC), 25 | expected: false, 26 | }, 27 | { 28 | name: "date changes case[1]", 29 | spec: ScheduleSpec{ScheduleType: Daily, StartTime: "23:00", EndTime: "03:00"}, 30 | now: time.Date(2018, 9, 2, 02, 00, 0, 0, time.UTC), 31 | expected: true, 32 | }, 33 | { 34 | name: "date changes case[2]", 35 | spec: ScheduleSpec{ScheduleType: Daily, StartTime: "23:00", EndTime: "01:00"}, 36 | now: time.Date(2018, 9, 2, 02, 00, 0, 0, time.UTC), 37 | expected: false, 38 | }, 39 | } 40 | 41 | for _, tt := range tests { 42 | tt := tt 43 | t.Run(tt.name, func(t *testing.T) { 44 | contains, err := tt.spec.Contains(tt.now) 45 | if err != nil { 46 | t.Error(err) 47 | 48 | return 49 | } 50 | 51 | if contains != tt.expected { 52 | t.Errorf("%s is not expected condition. actual:%t expected:%t time: %s - %s", 53 | tt.now, contains, tt.expected, 54 | tt.spec.StartTime, tt.spec.EndTime) 55 | } 56 | }) 57 | } 58 | } 59 | 60 | func TestScheduleSpecContainsOneShot(t *testing.T) { 61 | tests := []struct { 62 | name string 63 | spec ScheduleSpec 64 | now time.Time 65 | expected bool 66 | }{ 67 | { 68 | name: "case[1]", 69 | spec: ScheduleSpec{ScheduleType: OneShot, StartTime: "2018-09-01T10:00", EndTime: "2018-09-10T19:00"}, 70 | now: time.Date(2018, 9, 10, 11, 00, 0, 0, time.UTC), 71 | expected: true, 72 | }, 73 | { 74 | name: "case[2]", 75 | spec: ScheduleSpec{ScheduleType: OneShot, StartTime: "2018-09-01T10:00", EndTime: "2018-09-05T19:00"}, 76 | now: time.Date(2018, 9, 10, 20, 00, 0, 0, time.UTC), 77 | expected: false, 78 | }, 79 | } 80 | 81 | for _, tt := range tests { 82 | tt := tt 83 | t.Run(tt.name, func(t *testing.T) { 84 | contains, err := tt.spec.Contains(tt.now) 85 | if err != nil { 86 | t.Error(err) 87 | 88 | return 89 | } 90 | 91 | if contains != tt.expected { 92 | t.Errorf("%s is not expected condition. actual:%t expected:%t time: %s - %s", 93 | tt.now, contains, tt.expected, 94 | tt.spec.StartTime, tt.spec.EndTime) 95 | } 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /apis/autoscaling/v1/contains_weekly_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestScheduleSpecContainsWeekly(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | spec ScheduleSpec 12 | now time.Time 13 | expected bool 14 | }{ 15 | // 1 day case 16 | { 17 | name: "1day case[1]", 18 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "10:00", EndTime: "19:00"}, 19 | now: time.Date(2018, 9, 2, 11, 00, 0, 0, time.UTC), 20 | expected: false, 21 | }, 22 | { 23 | name: "1day case[2]", 24 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "10:00", EndTime: "19:00"}, 25 | now: time.Date(2018, 9, 3, 9, 00, 0, 0, time.UTC), 26 | expected: false, 27 | }, 28 | { 29 | name: "1day case[3]", 30 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "10:00", EndTime: "19:00"}, 31 | now: time.Date(2018, 9, 3, 10, 00, 0, 0, time.UTC), 32 | expected: true, 33 | }, 34 | { 35 | name: "1day case[4]", 36 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "10:00", EndTime: "19:00"}, 37 | now: time.Date(2018, 9, 3, 11, 00, 0, 0, time.UTC), 38 | expected: true, 39 | }, 40 | { 41 | name: "1day case[5]", 42 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "10:00", EndTime: "19:00"}, 43 | now: time.Date(2018, 9, 3, 18, 00, 0, 0, time.UTC), 44 | expected: true, 45 | }, 46 | { 47 | name: "1day case[6]", 48 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "10:00", EndTime: "19:00"}, 49 | now: time.Date(2018, 9, 3, 19, 00, 0, 0, time.UTC), 50 | expected: false, 51 | }, 52 | { 53 | name: "1day case[7]", 54 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "10:00", EndTime: "19:00"}, 55 | now: time.Date(2018, 9, 3, 20, 00, 0, 0, time.UTC), 56 | expected: false, 57 | }, 58 | { 59 | name: "1day case[8]", 60 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "10:00", EndTime: "19:00"}, 61 | now: time.Date(2018, 9, 4, 11, 00, 0, 0, time.UTC), 62 | expected: false, 63 | }, 64 | 65 | // 1 day case with minutes 66 | { 67 | name: "1day case with minutes[1]", 68 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "10:00", EndTime: "10:30"}, 69 | now: time.Date(2018, 9, 3, 9, 50, 0, 0, time.UTC), 70 | expected: false, 71 | }, 72 | { 73 | name: "1day case with minutes[2]", 74 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "10:00", EndTime: "10:30"}, 75 | now: time.Date(2018, 9, 3, 10, 20, 0, 0, time.UTC), 76 | expected: true, 77 | }, 78 | { 79 | name: "1day case with minutes[3]", 80 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "10:00", EndTime: "10:30"}, 81 | now: time.Date(2018, 9, 3, 9, 50, 0, 0, time.UTC), 82 | expected: false, 83 | }, 84 | { 85 | name: "1day case with minutes[4]", 86 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "10:00", EndTime: "10:30"}, 87 | now: time.Date(2018, 9, 3, 10, 40, 0, 0, time.UTC), 88 | expected: false, 89 | }, 90 | 91 | // EndTime is over 24 o'clock. 92 | { 93 | name: "EndTime is over 24o'clock[1]", 94 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "15:00", EndTime: "01:00"}, 95 | now: time.Date(2018, 9, 3, 0, 00, 0, 0, time.UTC), 96 | expected: false, 97 | }, 98 | { 99 | name: "EndTime is over 24o'clock[2]", 100 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "15:00", EndTime: "01:00"}, 101 | now: time.Date(2018, 9, 3, 14, 00, 0, 0, time.UTC), 102 | expected: false, 103 | }, 104 | { 105 | name: "EndTime is over 24o'clock[3]", 106 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "15:00", EndTime: "01:00"}, 107 | now: time.Date(2018, 9, 3, 15, 00, 0, 0, time.UTC), 108 | expected: true, 109 | }, 110 | { 111 | name: "EndTime is over 24o'clock[4]", 112 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "15:00", EndTime: "01:00"}, 113 | now: time.Date(2018, 9, 3, 19, 00, 0, 0, time.UTC), 114 | expected: true, 115 | }, 116 | { 117 | name: "EndTime is over 24o'clock[5]", 118 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "15:00", EndTime: "01:00"}, 119 | now: time.Date(2018, 9, 4, 0, 00, 0, 0, time.UTC), 120 | expected: true, 121 | }, 122 | { 123 | name: "EndTime is over 24o'clock[6]", 124 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "15:00", EndTime: "01:00"}, 125 | now: time.Date(2018, 9, 4, 1, 00, 0, 0, time.UTC), 126 | expected: false, 127 | }, 128 | { 129 | name: "EndTime is over 24o'clock[7]", 130 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "15:00", EndTime: "01:00"}, 131 | now: time.Date(2018, 9, 4, 2, 00, 0, 0, time.UTC), 132 | expected: false, 133 | }, 134 | 135 | // EndTime is over 24 o'clock with minutes case. 136 | { 137 | name: "EndTime is over 24o'clock with minutes case[1]", 138 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "15:00", EndTime: "01:30"}, 139 | now: time.Date(2018, 9, 4, 1, 20, 0, 0, time.UTC), 140 | expected: true, 141 | }, 142 | { 143 | name: "EndTime is over 24o'clock with minutes case[2]", 144 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "15:00", EndTime: "01:30"}, 145 | now: time.Date(2018, 9, 4, 1, 30, 0, 0, time.UTC), 146 | expected: false, 147 | }, 148 | { 149 | name: "EndTime is over 24o'clock with minutes case[3]", 150 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Monday", StartTime: "15:00", EndTime: "01:30"}, 151 | now: time.Date(2018, 9, 5, 1, 20, 0, 0, time.UTC), 152 | expected: false, 153 | }, 154 | 155 | // EndTime is over 24 o'clock, 2 days. 156 | { 157 | name: "EndTime is over 24o'clock 2days[1]", 158 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Tuesday", StartTime: "15:00", EndTime: "01:30"}, 159 | now: time.Date(2018, 9, 3, 16, 00, 0, 0, time.UTC), 160 | expected: true, 161 | }, 162 | { 163 | name: "EndTime is over 24o'clock 2days[2]", 164 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Tuesday", StartTime: "15:00", EndTime: "01:30"}, 165 | now: time.Date(2018, 9, 4, 16, 00, 0, 0, time.UTC), 166 | expected: true, 167 | }, 168 | { 169 | name: "EndTime is over 24o'clock 2days[3]", 170 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Tuesday", StartTime: "15:00", EndTime: "01:30"}, 171 | now: time.Date(2018, 9, 5, 1, 00, 0, 0, time.UTC), 172 | expected: true, 173 | }, 174 | { 175 | name: "EndTime is over 24o'clock 2days[4]", 176 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Tuesday", StartTime: "15:00", EndTime: "01:30"}, 177 | now: time.Date(2018, 9, 5, 2, 00, 0, 0, time.UTC), 178 | expected: false, 179 | }, 180 | { 181 | name: "EndTime is over 24o'clock 2days[5]", 182 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Tuesday", StartTime: "15:00", EndTime: "01:30"}, 183 | now: time.Date(2018, 9, 6, 16, 00, 0, 0, time.UTC), 184 | expected: false, 185 | }, 186 | 187 | // the middle day in 3 days 188 | { 189 | name: "the middle day in 3days[1]", 190 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Wednesday", StartTime: "15:00", EndTime: "01:30"}, 191 | now: time.Date(2018, 9, 4, 14, 00, 0, 0, time.UTC), 192 | expected: false, 193 | }, 194 | { 195 | name: "the middle day in 3days[2]", 196 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Wednesday", StartTime: "15:00", EndTime: "01:30"}, 197 | now: time.Date(2018, 9, 4, 15, 00, 0, 0, time.UTC), 198 | expected: true, 199 | }, 200 | { 201 | name: "the middle day in 3days[3]", 202 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Wednesday", StartTime: "15:00", EndTime: "01:30"}, 203 | now: time.Date(2018, 9, 4, 16, 00, 0, 0, time.UTC), 204 | expected: true, 205 | }, 206 | { 207 | name: "the middle day in 3days[4]", 208 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Wednesday", StartTime: "15:00", EndTime: "01:30"}, 209 | now: time.Date(2018, 9, 4, 23, 00, 0, 0, time.UTC), 210 | expected: true, 211 | }, 212 | { 213 | name: "the middle day in 3days[5]", 214 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Wednesday", StartTime: "15:00", EndTime: "01:30"}, 215 | now: time.Date(2018, 9, 5, 0, 00, 0, 0, time.UTC), 216 | expected: true, 217 | }, 218 | { 219 | name: "the middle day in 3days[6]", 220 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Wednesday", StartTime: "15:00", EndTime: "01:30"}, 221 | now: time.Date(2018, 9, 5, 1, 00, 0, 0, time.UTC), 222 | expected: true, 223 | }, 224 | { 225 | name: "the middle day in 3days[7]", 226 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Monday", EndDayOfWeek: "Wednesday", StartTime: "15:00", EndTime: "01:30"}, 227 | now: time.Date(2018, 9, 5, 2, 00, 0, 0, time.UTC), 228 | expected: false, 229 | }, 230 | 231 | // StartDayOfWeek is bigger than EndDayOfWeek 232 | { 233 | name: "StartDayOfWeek is bigger than EndDayOfWeek[1]", 234 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Tuesday", EndDayOfWeek: "Sunday", StartTime: "15:00", EndTime: "01:30"}, 235 | now: time.Date(2018, 9, 3, 1, 20, 0, 0, time.UTC), 236 | expected: true, 237 | }, 238 | { 239 | name: "StartDayOfWeek is bigger than EndDayOfWeek[2]", 240 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Tuesday", EndDayOfWeek: "Sunday", StartTime: "15:00", EndTime: "01:30"}, 241 | now: time.Date(2018, 9, 3, 16, 20, 0, 0, time.UTC), 242 | expected: false, 243 | }, 244 | { 245 | name: "StartDayOfWeek is bigger than EndDayOfWeek[3]", 246 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Tuesday", EndDayOfWeek: "Sunday", StartTime: "15:00", EndTime: "01:30"}, 247 | now: time.Date(2018, 9, 4, 1, 20, 0, 0, time.UTC), 248 | expected: false, 249 | }, 250 | { 251 | name: "StartDayOfWeek is bigger than EndDayOfWeek[4]", 252 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Tuesday", EndDayOfWeek: "Sunday", StartTime: "15:00", EndTime: "01:30"}, 253 | now: time.Date(2018, 9, 5, 1, 20, 0, 0, time.UTC), 254 | expected: true, 255 | }, 256 | 257 | // everyday 258 | { 259 | name: "everyday[1]", 260 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Sunday", EndDayOfWeek: "Saturday", StartTime: "15:00", EndTime: "01:30"}, 261 | now: time.Date(2018, 9, 3, 1, 20, 0, 0, time.UTC), 262 | expected: true, 263 | }, 264 | { 265 | name: "everyday[2]", 266 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Sunday", EndDayOfWeek: "Saturday", StartTime: "15:00", EndTime: "01:30"}, 267 | now: time.Date(2018, 9, 3, 16, 00, 0, 0, time.UTC), 268 | expected: true, 269 | }, 270 | { 271 | name: "everyday[3]", 272 | spec: ScheduleSpec{ScheduleType: Weekly, StartDayOfWeek: "Sunday", EndDayOfWeek: "Saturday", StartTime: "15:00", EndTime: "01:30"}, 273 | now: time.Date(2018, 9, 5, 1, 20, 0, 0, time.UTC), 274 | expected: true, 275 | }, 276 | } 277 | 278 | for _, tt := range tests { 279 | tt := tt 280 | t.Run(tt.name, func(t *testing.T) { 281 | contains, err := tt.spec.Contains(tt.now) 282 | if err != nil { 283 | t.Error(err) 284 | 285 | return 286 | } 287 | 288 | if contains != tt.expected { 289 | t.Errorf("%s is not expected condition. actual:%t expected:%t time: %s - %s day: %s - %s", 290 | tt.now, contains, tt.expected, 291 | tt.spec.StartTime, tt.spec.EndTime, 292 | tt.spec.StartDayOfWeek, tt.spec.EndDayOfWeek) 293 | t.Errorf("%s is %s", tt.now, tt.now.Weekday()) 294 | } 295 | }) 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /apis/autoscaling/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 autoscaling v1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=autoscaling.d-kuro.github.io 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: "autoscaling.d-kuro.github.io", 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 | -------------------------------------------------------------------------------- /apis/autoscaling/v1/schedule_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 | import ( 20 | "time" 21 | 22 | autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | ) 25 | 26 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 27 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 28 | 29 | // ScheduleSpec defines the desired state of Schedule. 30 | type ScheduleSpec struct { 31 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 32 | // Important: Run "make" to regenerate code after modifying this file 33 | 34 | // ScaleTargetRef points to the target resource to scale, and is used to the pods for which metrics 35 | // should be collected, as well as to actually change the replica count. 36 | // +kubebuiler:validation:Required 37 | ScaleTargetRef autoscalingv2beta2.CrossVersionObjectReference `json:"scaleTargetRef"` 38 | 39 | // Suspend indicates whether to suspend this schedule. (default is false) 40 | // +optional 41 | Suspend bool `json:"suspend"` 42 | 43 | // Description is schedule description. 44 | // +optional 45 | Description string `json:"description,omitempty"` 46 | 47 | // TimeZone is the name of the timezone used in the argument of the time.LoadLocation(name string) function. 48 | // StartTime and EndTime are interpreted as the time in the time zone specified by TimeZone. 49 | // If not specified, the time will be interpreted as UTC. 50 | // +optional 51 | TimeZone string `json:"timeZone,omitempty"` 52 | 53 | // MinReplicas is the lower limit for the number of replicas to which the autoscaler can scale down. 54 | // It defaults to 1 pod. 55 | // +kubebuilder:validation:Minimum=1 56 | // +optional 57 | MinReplicas *int32 `json:"minReplicas,omitempty"` 58 | 59 | // MaxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up. 60 | // +kubebuilder:validation:Minimum=1 61 | // +optional 62 | MaxReplicas *int32 `json:"maxReplicas,omitempty"` 63 | 64 | // ScheduleType is a type of schedule represented by "Weekly", "Daily", "OneShot". 65 | // +kubebuiler:validation:Required 66 | // +kubebuilder:validation:Enum=Weekly;Daily;OneShot 67 | ScheduleType ScheduleType `json:"type"` 68 | 69 | // StartDayOfWeek is scaling start day of week. 70 | // Represented by "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday". 71 | // +kubebuilder:validation:Enum=Sunday;Monday;Tuesday;Wednesday;Thursday;Friday;Saturday;"" 72 | // +optional 73 | StartDayOfWeek string `json:"startDayOfWeek"` 74 | 75 | // EndDayOfWeek is scaling end day of week. 76 | // Represented by "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday". 77 | // +kubebuilder:validation:Enum=Sunday;Monday;Tuesday;Wednesday;Thursday;Friday;Saturday;"" 78 | // +optional 79 | EndDayOfWeek string `json:"endDayOfWeek"` 80 | 81 | // StartTime is scaling start time. Defined in RFC3339 based format. 82 | // Different formats are evaluated depending on ScheduleType. 83 | // e.g. OneShot(yyyy-MM-ddTHH:mm), Weekly(HH:mm), Daily(HH:mm) 84 | // +kubebuiler:validation:Required 85 | StartTime string `json:"startTime"` 86 | 87 | // EndTime is scaling end time. Defined in RFC3339 based format. 88 | // Different formats are evaluated depending on ScheduleType. 89 | // e.g. OneShot(yyyy-MM-ddTHH:mm), Weekly(HH:mm), Daily(HH:mm) 90 | // +kubebuiler:validation:Required 91 | EndTime string `json:"endTime"` 92 | } 93 | 94 | type ScheduleType string 95 | 96 | const ( 97 | Weekly ScheduleType = "Weekly" 98 | Daily ScheduleType = "Daily" 99 | OneShot ScheduleType = "OneShot" 100 | ) 101 | 102 | type ScheduleConditionType string 103 | 104 | const ( 105 | ScheduleAvailable ScheduleConditionType = "Available" 106 | ScheduleSuspend ScheduleConditionType = "Suspend" 107 | ScheduleProgressing ScheduleConditionType = "Progressing" 108 | ScheduleDegraded ScheduleConditionType = "Degraded" 109 | ScheduleCompleted ScheduleConditionType = "Completed" 110 | ) 111 | 112 | func (s ScheduleSpec) IsCompleted(now time.Time) (bool, error) { 113 | if s.ScheduleType != OneShot { 114 | return false, nil 115 | } 116 | 117 | endTime, err := time.ParseInLocation("2006-01-02T15:04", s.EndTime, now.Location()) 118 | if err != nil { 119 | return false, err 120 | } 121 | 122 | return now.After(endTime), nil 123 | } 124 | 125 | // ScheduleStatus defines the observed state of Schedule. 126 | type ScheduleStatus struct { 127 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 128 | // Important: Run "make" to regenerate code after modifying this file 129 | 130 | // LastTransitionTime is the last time the condition transitioned from one status to another. 131 | // +kubebuilder:validation:Type=string 132 | // +kubebuilder:validation:Format=date-time 133 | // +optional 134 | LastTransitionTime metav1.Time `json:"lastTransitionTime"` 135 | 136 | // Condition is schedule status type. 137 | // +optional 138 | Condition ScheduleConditionType `json:"condition,omitempty"` 139 | } 140 | 141 | // +kubebuilder:object:root=true 142 | // +kubebuilder:subresource:status 143 | // +kubebuilder:printcolumn:name="REFERENCE",type=string,JSONPath=`.spec.scaleTargetRef.name`,priority=0 144 | // +kubebuilder:printcolumn:name="TYPE",type=string,JSONPath=`.spec.type`,priority=0 145 | // +kubebuilder:printcolumn:name="STARTTIME",type=string,JSONPath=`.spec.startTime`,priority=0 146 | // +kubebuilder:printcolumn:name="ENDTIME",type=string,JSONPath=`.spec.endTime`,priority=0 147 | // +kubebuilder:printcolumn:name="STARTDAYOFWEEK",type=string,JSONPath=`.spec.startDayOfWeek`,priority=0 148 | // +kubebuilder:printcolumn:name="ENDDAYOFWEEK",type=string,JSONPath=`.spec.endDayOfWeek`,priority=0 149 | // +kubebuilder:printcolumn:name="MINPODS",type=integer,JSONPath=`.spec.minReplicas`,priority=1 150 | // +kubebuilder:printcolumn:name="MAXPODS",type=integer,JSONPath=`.spec.maxReplicas`,priority=1 151 | // +kubebuilder:printcolumn:name="STATUS",type=string,JSONPath=`.status.condition`,priority=0 152 | // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=`.metadata.creationTimestamp`,priority=0 153 | 154 | // Schedule is the Schema for the schedules API. 155 | type Schedule struct { 156 | metav1.TypeMeta `json:",inline"` 157 | metav1.ObjectMeta `json:"metadata,omitempty"` 158 | 159 | Spec ScheduleSpec `json:"spec,omitempty"` 160 | Status ScheduleStatus `json:"status,omitempty"` 161 | } 162 | 163 | // +kubebuilder:object:root=true 164 | 165 | // ScheduleList contains a list of Schedule. 166 | type ScheduleList struct { 167 | metav1.TypeMeta `json:",inline"` 168 | metav1.ListMeta `json:"metadata,omitempty"` 169 | Items []Schedule `json:"items"` 170 | } 171 | 172 | func init() { 173 | SchemeBuilder.Register(&Schedule{}, &ScheduleList{}) 174 | } 175 | -------------------------------------------------------------------------------- /apis/autoscaling/v1/schedule_types_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestScheduleIsCompleted(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | spec ScheduleSpec 12 | now time.Time 13 | expected bool 14 | }{ 15 | { 16 | name: "case[1]", 17 | spec: ScheduleSpec{ScheduleType: OneShot, StartTime: "2018-09-01T10:00", EndTime: "2018-09-10T19:00"}, 18 | now: time.Date(2018, 9, 10, 20, 00, 0, 0, time.UTC), 19 | expected: true, 20 | }, 21 | { 22 | name: "case[2]", 23 | spec: ScheduleSpec{ScheduleType: OneShot, StartTime: "2018-09-01T10:00", EndTime: "2018-09-10T19:00"}, 24 | now: time.Date(2018, 9, 10, 18, 59, 0, 0, time.UTC), 25 | expected: false, 26 | }, 27 | } 28 | 29 | for _, tt := range tests { 30 | tt := tt 31 | t.Run(tt.name, func(t *testing.T) { 32 | completed, err := tt.spec.IsCompleted(tt.now) 33 | if err != nil { 34 | t.Error(err) 35 | 36 | return 37 | } 38 | 39 | if completed != tt.expected { 40 | t.Errorf("%s is not expected condition. actual:%t, expected:%t, endTime: %s", 41 | tt.now, completed, tt.expected, tt.spec.EndTime) 42 | } 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /apis/autoscaling/v1/scheduledpodautoscaler_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 | import ( 20 | autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 25 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 26 | 27 | // ScheduledPodAutoscalerSpec defines the desired state of ScheduledPodAutoscaler. 28 | type ScheduledPodAutoscalerSpec struct { 29 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 30 | // Important: Run "make" to regenerate code after modifying this file 31 | 32 | // HorizontalPodAutoscalerSpec is HorizontalPodAutoscaler v2beta2 API spec. 33 | // ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#horizontalpodautoscaler-v2beta2-autoscaling 34 | // +kubebuilder:validation:Required 35 | HorizontalPodAutoscalerSpec autoscalingv2beta2.HorizontalPodAutoscalerSpec `json:"horizontalPodAutoscalerSpec"` 36 | } 37 | 38 | type ScheduledPodAutoscalerConditionType string 39 | 40 | const ( 41 | ScheduledPodAutoscalerAvailable ScheduledPodAutoscalerConditionType = "Available" 42 | ScheduledPodAutoscalerDegraded ScheduledPodAutoscalerConditionType = "Degraded" 43 | ) 44 | 45 | // ScheduledPodAutoscalerStatus defines the observed state of ScheduledPodAutoscaler. 46 | type ScheduledPodAutoscalerStatus struct { 47 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 48 | // Important: Run "make" to regenerate code after modifying this file 49 | 50 | // LastTransitionTime is the last time the condition transitioned from one status to another. 51 | // +kubebuilder:validation:Type=string 52 | // +kubebuilder:validation:Format=date-time 53 | // +optional 54 | LastTransitionTime metav1.Time `json:"lastTransitionTime"` 55 | 56 | // Condition is schedule status type. 57 | // +optional 58 | Condition ScheduledPodAutoscalerConditionType `json:"condition,omitempty"` 59 | } 60 | 61 | // +kubebuilder:object:root=true 62 | // +kubebuilder:resource:shortName=spa 63 | // +kubebuilder:subresource:status 64 | // +kubebuilder:printcolumn:name="MINPODS",type=integer,JSONPath=`.spec.horizontalPodAutoscalerSpec.minReplicas`,priority=0 65 | // +kubebuilder:printcolumn:name="MAXPODS",type=integer,JSONPath=`.spec.horizontalPodAutoscalerSpec.maxReplicas`,priority=0 66 | // +kubebuilder:printcolumn:name="STATUS",type=string,JSONPath=`.status.condition`,priority=0 67 | // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp",priority=0 68 | 69 | // ScheduledPodAutoscaler is the Schema for the scheduledpodautoscalers API. 70 | type ScheduledPodAutoscaler struct { 71 | metav1.TypeMeta `json:",inline"` 72 | metav1.ObjectMeta `json:"metadata,omitempty"` 73 | 74 | Spec ScheduledPodAutoscalerSpec `json:"spec,omitempty"` 75 | Status ScheduledPodAutoscalerStatus `json:"status,omitempty"` 76 | } 77 | 78 | // +kubebuilder:object:root=true 79 | 80 | // ScheduledPodAutoscalerList contains a list of ScheduledPodAutoscaler. 81 | type ScheduledPodAutoscalerList struct { 82 | metav1.TypeMeta `json:",inline"` 83 | metav1.ListMeta `json:"metadata,omitempty"` 84 | Items []ScheduledPodAutoscaler `json:"items"` 85 | } 86 | 87 | func init() { 88 | SchemeBuilder.Register(&ScheduledPodAutoscaler{}, &ScheduledPodAutoscalerList{}) 89 | } 90 | -------------------------------------------------------------------------------- /apis/autoscaling/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | /* 4 | 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 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | ) 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *Schedule) DeepCopyInto(out *Schedule) { 29 | *out = *in 30 | out.TypeMeta = in.TypeMeta 31 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 32 | in.Spec.DeepCopyInto(&out.Spec) 33 | in.Status.DeepCopyInto(&out.Status) 34 | } 35 | 36 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Schedule. 37 | func (in *Schedule) DeepCopy() *Schedule { 38 | if in == nil { 39 | return nil 40 | } 41 | out := new(Schedule) 42 | in.DeepCopyInto(out) 43 | return out 44 | } 45 | 46 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 47 | func (in *Schedule) DeepCopyObject() runtime.Object { 48 | if c := in.DeepCopy(); c != nil { 49 | return c 50 | } 51 | return nil 52 | } 53 | 54 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 55 | func (in *ScheduleList) DeepCopyInto(out *ScheduleList) { 56 | *out = *in 57 | out.TypeMeta = in.TypeMeta 58 | in.ListMeta.DeepCopyInto(&out.ListMeta) 59 | if in.Items != nil { 60 | in, out := &in.Items, &out.Items 61 | *out = make([]Schedule, len(*in)) 62 | for i := range *in { 63 | (*in)[i].DeepCopyInto(&(*out)[i]) 64 | } 65 | } 66 | } 67 | 68 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScheduleList. 69 | func (in *ScheduleList) DeepCopy() *ScheduleList { 70 | if in == nil { 71 | return nil 72 | } 73 | out := new(ScheduleList) 74 | in.DeepCopyInto(out) 75 | return out 76 | } 77 | 78 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 79 | func (in *ScheduleList) DeepCopyObject() runtime.Object { 80 | if c := in.DeepCopy(); c != nil { 81 | return c 82 | } 83 | return nil 84 | } 85 | 86 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 87 | func (in *ScheduleSpec) DeepCopyInto(out *ScheduleSpec) { 88 | *out = *in 89 | out.ScaleTargetRef = in.ScaleTargetRef 90 | if in.MinReplicas != nil { 91 | in, out := &in.MinReplicas, &out.MinReplicas 92 | *out = new(int32) 93 | **out = **in 94 | } 95 | if in.MaxReplicas != nil { 96 | in, out := &in.MaxReplicas, &out.MaxReplicas 97 | *out = new(int32) 98 | **out = **in 99 | } 100 | } 101 | 102 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScheduleSpec. 103 | func (in *ScheduleSpec) DeepCopy() *ScheduleSpec { 104 | if in == nil { 105 | return nil 106 | } 107 | out := new(ScheduleSpec) 108 | in.DeepCopyInto(out) 109 | return out 110 | } 111 | 112 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 113 | func (in *ScheduleStatus) DeepCopyInto(out *ScheduleStatus) { 114 | *out = *in 115 | in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) 116 | } 117 | 118 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScheduleStatus. 119 | func (in *ScheduleStatus) DeepCopy() *ScheduleStatus { 120 | if in == nil { 121 | return nil 122 | } 123 | out := new(ScheduleStatus) 124 | in.DeepCopyInto(out) 125 | return out 126 | } 127 | 128 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 129 | func (in *ScheduledPodAutoscaler) DeepCopyInto(out *ScheduledPodAutoscaler) { 130 | *out = *in 131 | out.TypeMeta = in.TypeMeta 132 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 133 | in.Spec.DeepCopyInto(&out.Spec) 134 | in.Status.DeepCopyInto(&out.Status) 135 | } 136 | 137 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScheduledPodAutoscaler. 138 | func (in *ScheduledPodAutoscaler) DeepCopy() *ScheduledPodAutoscaler { 139 | if in == nil { 140 | return nil 141 | } 142 | out := new(ScheduledPodAutoscaler) 143 | in.DeepCopyInto(out) 144 | return out 145 | } 146 | 147 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 148 | func (in *ScheduledPodAutoscaler) DeepCopyObject() runtime.Object { 149 | if c := in.DeepCopy(); c != nil { 150 | return c 151 | } 152 | return nil 153 | } 154 | 155 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 156 | func (in *ScheduledPodAutoscalerList) DeepCopyInto(out *ScheduledPodAutoscalerList) { 157 | *out = *in 158 | out.TypeMeta = in.TypeMeta 159 | in.ListMeta.DeepCopyInto(&out.ListMeta) 160 | if in.Items != nil { 161 | in, out := &in.Items, &out.Items 162 | *out = make([]ScheduledPodAutoscaler, len(*in)) 163 | for i := range *in { 164 | (*in)[i].DeepCopyInto(&(*out)[i]) 165 | } 166 | } 167 | } 168 | 169 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScheduledPodAutoscalerList. 170 | func (in *ScheduledPodAutoscalerList) DeepCopy() *ScheduledPodAutoscalerList { 171 | if in == nil { 172 | return nil 173 | } 174 | out := new(ScheduledPodAutoscalerList) 175 | in.DeepCopyInto(out) 176 | return out 177 | } 178 | 179 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 180 | func (in *ScheduledPodAutoscalerList) DeepCopyObject() runtime.Object { 181 | if c := in.DeepCopy(); c != nil { 182 | return c 183 | } 184 | return nil 185 | } 186 | 187 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 188 | func (in *ScheduledPodAutoscalerSpec) DeepCopyInto(out *ScheduledPodAutoscalerSpec) { 189 | *out = *in 190 | in.HorizontalPodAutoscalerSpec.DeepCopyInto(&out.HorizontalPodAutoscalerSpec) 191 | } 192 | 193 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScheduledPodAutoscalerSpec. 194 | func (in *ScheduledPodAutoscalerSpec) DeepCopy() *ScheduledPodAutoscalerSpec { 195 | if in == nil { 196 | return nil 197 | } 198 | out := new(ScheduledPodAutoscalerSpec) 199 | in.DeepCopyInto(out) 200 | return out 201 | } 202 | 203 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 204 | func (in *ScheduledPodAutoscalerStatus) DeepCopyInto(out *ScheduledPodAutoscalerStatus) { 205 | *out = *in 206 | in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) 207 | } 208 | 209 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScheduledPodAutoscalerStatus. 210 | func (in *ScheduledPodAutoscalerStatus) DeepCopy() *ScheduledPodAutoscalerStatus { 211 | if in == nil { 212 | return nil 213 | } 214 | out := new(ScheduledPodAutoscalerStatus) 215 | in.DeepCopyInto(out) 216 | return out 217 | } 218 | -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for 4 | # breaking changes 5 | apiVersion: cert-manager.io/v1alpha2 6 | kind: Issuer 7 | metadata: 8 | name: selfsigned-issuer 9 | namespace: system 10 | spec: 11 | selfSigned: {} 12 | --- 13 | apiVersion: cert-manager.io/v1alpha2 14 | kind: Certificate 15 | metadata: 16 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 17 | namespace: system 18 | spec: 19 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 20 | dnsNames: 21 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 22 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 23 | issuerRef: 24 | kind: Issuer 25 | name: selfsigned-issuer 26 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 27 | -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: cert-manager.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /config/crd/bases/autoscaling.d-kuro.github.io_schedules.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.4.1 8 | creationTimestamp: null 9 | name: schedules.autoscaling.d-kuro.github.io 10 | spec: 11 | group: autoscaling.d-kuro.github.io 12 | names: 13 | kind: Schedule 14 | listKind: ScheduleList 15 | plural: schedules 16 | singular: schedule 17 | scope: Namespaced 18 | versions: 19 | - additionalPrinterColumns: 20 | - jsonPath: .spec.scaleTargetRef.name 21 | name: REFERENCE 22 | type: string 23 | - jsonPath: .spec.type 24 | name: TYPE 25 | type: string 26 | - jsonPath: .spec.startTime 27 | name: STARTTIME 28 | type: string 29 | - jsonPath: .spec.endTime 30 | name: ENDTIME 31 | type: string 32 | - jsonPath: .spec.startDayOfWeek 33 | name: STARTDAYOFWEEK 34 | type: string 35 | - jsonPath: .spec.endDayOfWeek 36 | name: ENDDAYOFWEEK 37 | type: string 38 | - jsonPath: .spec.minReplicas 39 | name: MINPODS 40 | priority: 1 41 | type: integer 42 | - jsonPath: .spec.maxReplicas 43 | name: MAXPODS 44 | priority: 1 45 | type: integer 46 | - jsonPath: .status.condition 47 | name: STATUS 48 | type: string 49 | - jsonPath: .metadata.creationTimestamp 50 | name: AGE 51 | type: date 52 | name: v1 53 | schema: 54 | openAPIV3Schema: 55 | description: Schedule is the Schema for the schedules API. 56 | properties: 57 | apiVersion: 58 | description: 'APIVersion defines the versioned schema of this representation 59 | of an object. Servers should convert recognized schemas to the latest 60 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 61 | type: string 62 | kind: 63 | description: 'Kind is a string value representing the REST resource this 64 | object represents. Servers may infer this from the endpoint the client 65 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 66 | type: string 67 | metadata: 68 | type: object 69 | spec: 70 | description: ScheduleSpec defines the desired state of Schedule. 71 | properties: 72 | description: 73 | description: Description is schedule description. 74 | type: string 75 | endDayOfWeek: 76 | description: EndDayOfWeek is scaling end day of week. Represented 77 | by "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", 78 | "Saturday". 79 | enum: 80 | - Sunday 81 | - Monday 82 | - Tuesday 83 | - Wednesday 84 | - Thursday 85 | - Friday 86 | - Saturday 87 | - "" 88 | type: string 89 | endTime: 90 | description: EndTime is scaling end time. Defined in RFC3339 based 91 | format. Different formats are evaluated depending on ScheduleType. 92 | e.g. OneShot(yyyy-MM-ddTHH:mm), Weekly(HH:mm), Daily(HH:mm) 93 | type: string 94 | maxReplicas: 95 | description: MaxReplicas is the upper limit for the number of replicas 96 | to which the autoscaler can scale up. 97 | format: int32 98 | minimum: 1 99 | type: integer 100 | minReplicas: 101 | description: MinReplicas is the lower limit for the number of replicas 102 | to which the autoscaler can scale down. It defaults to 1 pod. 103 | format: int32 104 | minimum: 1 105 | type: integer 106 | scaleTargetRef: 107 | description: ScaleTargetRef points to the target resource to scale, 108 | and is used to the pods for which metrics should be collected, as 109 | well as to actually change the replica count. 110 | properties: 111 | apiVersion: 112 | description: API version of the referent 113 | type: string 114 | kind: 115 | description: 'Kind of the referent; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds"' 116 | type: string 117 | name: 118 | description: 'Name of the referent; More info: http://kubernetes.io/docs/user-guide/identifiers#names' 119 | type: string 120 | required: 121 | - kind 122 | - name 123 | type: object 124 | startDayOfWeek: 125 | description: StartDayOfWeek is scaling start day of week. Represented 126 | by "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", 127 | "Saturday". 128 | enum: 129 | - Sunday 130 | - Monday 131 | - Tuesday 132 | - Wednesday 133 | - Thursday 134 | - Friday 135 | - Saturday 136 | - "" 137 | type: string 138 | startTime: 139 | description: StartTime is scaling start time. Defined in RFC3339 based 140 | format. Different formats are evaluated depending on ScheduleType. 141 | e.g. OneShot(yyyy-MM-ddTHH:mm), Weekly(HH:mm), Daily(HH:mm) 142 | type: string 143 | suspend: 144 | description: Suspend indicates whether to suspend this schedule. (default 145 | is false) 146 | type: boolean 147 | timeZone: 148 | description: TimeZone is the name of the timezone used in the argument 149 | of the time.LoadLocation(name string) function. StartTime and EndTime 150 | are interpreted as the time in the time zone specified by TimeZone. 151 | If not specified, the time will be interpreted as UTC. 152 | type: string 153 | type: 154 | description: ScheduleType is a type of schedule represented by "Weekly", 155 | "Daily", "OneShot". 156 | enum: 157 | - Weekly 158 | - Daily 159 | - OneShot 160 | type: string 161 | required: 162 | - endTime 163 | - scaleTargetRef 164 | - startTime 165 | - type 166 | type: object 167 | status: 168 | description: ScheduleStatus defines the observed state of Schedule. 169 | properties: 170 | condition: 171 | description: Condition is schedule status type. 172 | type: string 173 | lastTransitionTime: 174 | description: LastTransitionTime is the last time the condition transitioned 175 | from one status to another. 176 | format: date-time 177 | type: string 178 | type: object 179 | type: object 180 | served: true 181 | storage: true 182 | subresources: 183 | status: {} 184 | status: 185 | acceptedNames: 186 | kind: "" 187 | plural: "" 188 | conditions: [] 189 | storedVersions: [] 190 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/autoscaling.d-kuro.github.io_scheduledpodautoscalers.yaml 6 | - bases/autoscaling.d-kuro.github.io_schedules.yaml 7 | # +kubebuilder:scaffold:crdkustomizeresource 8 | 9 | patchesStrategicMerge: 10 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 11 | # patches here are for enabling the conversion webhook for each CRD 12 | #- patches/webhook_in_scheduledpodautoscalers.yaml 13 | #- patches/webhook_in_schedules.yaml 14 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 15 | 16 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 17 | # patches here are for enabling the CA injection for each CRD 18 | #- patches/cainjection_in_scheduledpodautoscalers.yaml 19 | #- patches/cainjection_in_schedules.yaml 20 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 21 | 22 | # the following config is for teaching kustomize how to do kustomization for CRDs. 23 | configurations: 24 | - kustomizeconfig.yaml 25 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | group: apiextensions.k8s.io 8 | path: spec/conversion/webhookClientConfig/service/name 9 | 10 | namespace: 11 | - kind: CustomResourceDefinition 12 | group: apiextensions.k8s.io 13 | path: spec/conversion/webhookClientConfig/service/namespace 14 | create: false 15 | 16 | varReference: 17 | - path: metadata/annotations 18 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_scheduledpodautoscalers.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: scheduledpodautoscalers.autoscaling.d-kuro.github.io 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_schedules.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: schedules.autoscaling.d-kuro.github.io 9 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_scheduledpodautoscalers.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: scheduledpodautoscalers.autoscaling.d-kuro.github.io 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_schedules.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: schedules.autoscaling.d-kuro.github.io 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: scheduled-pod-autoscaler-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: scheduled-pod-autoscaler- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 20 | # crd/kustomization.yaml 21 | #- ../webhook 22 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 23 | #- ../certmanager 24 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 25 | #- ../prometheus 26 | 27 | patchesStrategicMerge: 28 | # Protect the /metrics endpoint by putting it behind auth. 29 | # If you want your controller-manager to expose the /metrics 30 | # endpoint w/o any authn/z, please comment the following line. 31 | - manager_auth_proxy_patch.yaml 32 | 33 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 34 | # crd/kustomization.yaml 35 | #- manager_webhook_patch.yaml 36 | 37 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 38 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 39 | # 'CERTMANAGER' needs to be enabled to use ca injection 40 | #- webhookcainjection_patch.yaml 41 | 42 | # the following config is for teaching kustomize how to do var substitution 43 | vars: 44 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 45 | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 46 | # objref: 47 | # kind: Certificate 48 | # group: cert-manager.io 49 | # version: v1alpha2 50 | # name: serving-cert # this name should match the one in certificate.yaml 51 | # fieldref: 52 | # fieldpath: metadata.namespace 53 | #- name: CERTIFICATE_NAME 54 | # objref: 55 | # kind: Certificate 56 | # group: cert-manager.io 57 | # version: v1alpha2 58 | # name: serving-cert # this name should match the one in certificate.yaml 59 | #- name: SERVICE_NAMESPACE # namespace of the service 60 | # objref: 61 | # kind: Service 62 | # version: v1 63 | # name: webhook-service 64 | # fieldref: 65 | # fieldpath: metadata.namespace 66 | #- name: SERVICE_NAME 67 | # objref: 68 | # kind: Service 69 | # version: v1 70 | # name: webhook-service 71 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=10" 19 | ports: 20 | - containerPort: 8443 21 | name: https 22 | - name: manager 23 | args: 24 | - "--metrics-addr=127.0.0.1:8080" 25 | - "--enable-leader-election" 26 | -------------------------------------------------------------------------------- /config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/k8s-webhook-server/serving-certs 17 | name: cert 18 | readOnly: true 19 | volumes: 20 | - name: cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. 3 | apiVersion: admissionregistration.k8s.io/v1beta1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | name: mutating-webhook-configuration 7 | annotations: 8 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 9 | --- 10 | apiVersion: admissionregistration.k8s.io/v1beta1 11 | kind: ValidatingWebhookConfiguration 12 | metadata: 13 | name: validating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 16 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: system 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: controller-manager 12 | namespace: system 13 | labels: 14 | control-plane: controller-manager 15 | spec: 16 | selector: 17 | matchLabels: 18 | control-plane: controller-manager 19 | replicas: 1 20 | template: 21 | metadata: 22 | labels: 23 | control-plane: controller-manager 24 | spec: 25 | containers: 26 | - command: 27 | - /manager 28 | args: 29 | - --enable-leader-election 30 | image: controller:latest 31 | name: manager 32 | resources: 33 | limits: 34 | cpu: 100m 35 | memory: 30Mi 36 | requests: 37 | cpu: 100m 38 | memory: 20Mi 39 | terminationGracePeriodSeconds: 10 40 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | selector: 15 | matchLabels: 16 | control-plane: controller-manager 17 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: ["/metrics"] 7 | verbs: ["get"] 8 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: ["authentication.k8s.io"] 7 | resources: 8 | - tokenreviews 9 | verbs: ["create"] 10 | - apiGroups: ["authorization.k8s.io"] 11 | resources: 12 | - subjectaccessreviews 13 | verbs: ["create"] 14 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - role.yaml 3 | - role_binding.yaml 4 | - leader_election_role.yaml 5 | - leader_election_role_binding.yaml 6 | # Comment the following 4 lines if you want to disable 7 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 8 | # which protects your /metrics endpoint. 9 | - auth_proxy_service.yaml 10 | - auth_proxy_role.yaml 11 | - auth_proxy_role_binding.yaml 12 | - auth_proxy_client_clusterrole.yaml 13 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - configmaps/status 23 | verbs: 24 | - get 25 | - update 26 | - patch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - events 31 | verbs: 32 | - create 33 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | creationTimestamp: null 7 | name: manager-role 8 | rules: 9 | - apiGroups: 10 | - autoscaling 11 | resources: 12 | - horizontalpodautoscalers 13 | verbs: 14 | - create 15 | - delete 16 | - get 17 | - list 18 | - patch 19 | - update 20 | - watch 21 | - apiGroups: 22 | - autoscaling.d-kuro.github.io 23 | resources: 24 | - scheduledpodautoscalers 25 | verbs: 26 | - create 27 | - delete 28 | - get 29 | - list 30 | - patch 31 | - update 32 | - watch 33 | - apiGroups: 34 | - autoscaling.d-kuro.github.io 35 | resources: 36 | - scheduledpodautoscalers/status 37 | verbs: 38 | - get 39 | - patch 40 | - update 41 | - apiGroups: 42 | - autoscaling.d-kuro.github.io 43 | resources: 44 | - schedules 45 | verbs: 46 | - create 47 | - delete 48 | - get 49 | - list 50 | - patch 51 | - update 52 | - watch 53 | - apiGroups: 54 | - autoscaling.d-kuro.github.io 55 | resources: 56 | - schedules/status 57 | verbs: 58 | - get 59 | - patch 60 | - update 61 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/schedule_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit schedules. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: schedule-editor-role 6 | rules: 7 | - apiGroups: 8 | - autoscaling.d-kuro.github.io 9 | resources: 10 | - schedules 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - autoscaling.d-kuro.github.io 21 | resources: 22 | - schedules/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/schedule_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view schedules. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: schedule-viewer-role 6 | rules: 7 | - apiGroups: 8 | - autoscaling.d-kuro.github.io 9 | resources: 10 | - schedules 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - autoscaling.d-kuro.github.io 17 | resources: 18 | - schedules/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/scheduledpodautoscaler_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit scheduledpodautoscalers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: scheduledpodautoscaler-editor-role 6 | rules: 7 | - apiGroups: 8 | - autoscaling.d-kuro.github.io 9 | resources: 10 | - scheduledpodautoscalers 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - autoscaling.d-kuro.github.io 21 | resources: 22 | - scheduledpodautoscalers/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/scheduledpodautoscaler_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view scheduledpodautoscalers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: scheduledpodautoscaler-viewer-role 6 | rules: 7 | - apiGroups: 8 | - autoscaling.d-kuro.github.io 9 | resources: 10 | - scheduledpodautoscalers 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - autoscaling.d-kuro.github.io 17 | resources: 18 | - scheduledpodautoscalers/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/samples/autoscaling_v1_schedule.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling.d-kuro.github.io/v1 2 | kind: Schedule 3 | metadata: 4 | name: schedule-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /config/samples/autoscaling_v1_scheduledpodautoscaler.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling.d-kuro.github.io/v1 2 | kind: ScheduledPodAutoscaler 3 | metadata: 4 | name: scheduledpodautoscaler-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | 24 | varReference: 25 | - path: metadata/annotations 26 | -------------------------------------------------------------------------------- /config/webhook/manifests.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-kuro/scheduled-pod-autoscaler/145803d1238ee41f620bd1e7ee0aa53f54803656/config/webhook/manifests.yaml -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: webhook-service 6 | namespace: system 7 | spec: 8 | ports: 9 | - port: 443 10 | targetPort: 9443 11 | selector: 12 | control-plane: controller-manager 13 | -------------------------------------------------------------------------------- /controllers/autoscaling/internal/testutil/testutil.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | func ToPointerInt32(value int) *int32 { 4 | i := int32(value) 5 | 6 | return &i 7 | } 8 | -------------------------------------------------------------------------------- /controllers/autoscaling/metrics.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "sigs.k8s.io/controller-runtime/pkg/metrics" 6 | ) 7 | 8 | var ( 9 | minReplicasCounter = prometheus.NewGaugeVec( 10 | prometheus.GaugeOpts{ 11 | Name: "scheduled_pod_auroscaler_min_replicas", 12 | Namespace: "scheduled_pod_auroscaler_controller", 13 | Help: "Lower limit for the number of pods that can be set by the scheduled pod autoscaler", 14 | }, 15 | []string{"name", "namespace"}, 16 | ) 17 | 18 | maxReplicasCounter = prometheus.NewGaugeVec( 19 | prometheus.GaugeOpts{ 20 | Name: "scheduled_pod_auroscaler_max_replicas", 21 | Namespace: "scheduled_pod_auroscaler_controller", 22 | Help: "Upper limit for the number of pods that can be set by the scheduled pod autoscaler", 23 | }, 24 | []string{"name", "namespace"}, 25 | ) 26 | ) 27 | 28 | func init() { 29 | metrics.Registry.MustRegister(minReplicasCounter, maxReplicasCounter) 30 | } 31 | -------------------------------------------------------------------------------- /controllers/autoscaling/schedule_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 | 22 | autoscalingv1 "github.com/d-kuro/scheduled-pod-autoscaler/apis/autoscaling/v1" 23 | "github.com/go-logr/logr" 24 | corev1 "k8s.io/api/core/v1" 25 | apierrors "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/types" 29 | "k8s.io/client-go/tools/record" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | ) 33 | 34 | // ScheduleReconciler reconciles a Schedule object. 35 | type ScheduleReconciler struct { 36 | client.Client 37 | Log logr.Logger 38 | Scheme *runtime.Scheme 39 | Recorder record.EventRecorder 40 | } 41 | 42 | // +kubebuilder:rbac:groups=autoscaling.d-kuro.github.io,resources=schedules,verbs=get;list;watch;create;update;patch;delete 43 | // +kubebuilder:rbac:groups=autoscaling.d-kuro.github.io,resources=schedules/status,verbs=get;update;patch 44 | 45 | func (r *ScheduleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 46 | log := r.Log.WithValues("schedule", req.NamespacedName) 47 | 48 | var schedule autoscalingv1.Schedule 49 | if err := r.Get(ctx, req.NamespacedName, &schedule); err != nil { 50 | if apierrors.IsNotFound(err) { 51 | return ctrl.Result{}, client.IgnoreNotFound(err) 52 | } 53 | 54 | log.Error(err, "unable to fetch Schedule") 55 | 56 | return ctrl.Result{}, err 57 | } 58 | 59 | if schedule.DeletionTimestamp != nil { 60 | return ctrl.Result{}, nil 61 | } 62 | 63 | namespacedName := types.NamespacedName{ 64 | Namespace: schedule.Namespace, 65 | Name: schedule.Spec.ScaleTargetRef.Name, 66 | } 67 | 68 | var spa autoscalingv1.ScheduledPodAutoscaler 69 | if err := r.Get(ctx, namespacedName, &spa); err != nil { 70 | log.Error(err, "unable to fetch ScheduledPodAutoscaler", "namespacedName", namespacedName) 71 | 72 | return ctrl.Result{}, err 73 | } 74 | 75 | if existing := metav1.GetControllerOf(&schedule); existing == nil { 76 | if err := ctrl.SetControllerReference(&spa, &schedule, r.Scheme); err != nil { 77 | log.Error(err, "unable to set ownerReference", "schedule", schedule) 78 | 79 | return ctrl.Result{}, err 80 | } 81 | 82 | if err := r.Update(ctx, &schedule, &client.UpdateOptions{}); err != nil { 83 | log.Error(err, "unable to update schedule", "schedule", schedule) 84 | 85 | return ctrl.Result{}, err 86 | } 87 | 88 | log.Info("successfully update Schedule", "schedule", schedule) 89 | } 90 | 91 | if schedule.Spec.Suspend { 92 | if schedule.Status.Condition != autoscalingv1.ScheduleSuspend { 93 | if err := r.updateScheduleStatus(ctx, log, schedule, autoscalingv1.ScheduleSuspend); err != nil { 94 | return ctrl.Result{}, err 95 | } 96 | } 97 | 98 | return ctrl.Result{}, nil 99 | } 100 | 101 | if schedule.Status.Condition != autoscalingv1.ScheduleSuspend && 102 | schedule.Status.Condition != autoscalingv1.ScheduleCompleted { 103 | if err := r.updateScheduleStatus(ctx, log, schedule, autoscalingv1.ScheduleAvailable); err != nil { 104 | return ctrl.Result{}, err 105 | } 106 | } 107 | 108 | return ctrl.Result{}, nil 109 | } 110 | 111 | func (r *ScheduleReconciler) updateScheduleStatus(ctx context.Context, log logr.Logger, 112 | schedule autoscalingv1.Schedule, newCondition autoscalingv1.ScheduleConditionType) error { 113 | if updated := setScheduleCondition(&schedule.Status, newCondition); updated { 114 | r.Recorder.Event(&schedule, corev1.EventTypeNormal, "Updated", "The schedule was updated.") 115 | 116 | if err := r.Status().Update(ctx, &schedule); err != nil { 117 | log.Error(err, "unable to update schedule status", "schedule", schedule) 118 | 119 | return err 120 | } 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func setScheduleCondition(status *autoscalingv1.ScheduleStatus, newCondition autoscalingv1.ScheduleConditionType) bool { 127 | updated := false 128 | 129 | if status.Condition == newCondition { 130 | return updated 131 | } 132 | 133 | status.Condition = newCondition 134 | status.LastTransitionTime = metav1.Now() 135 | updated = true 136 | 137 | return updated 138 | } 139 | 140 | func (r *ScheduleReconciler) SetupWithManager(mgr ctrl.Manager) error { 141 | return ctrl.NewControllerManagedBy(mgr). 142 | For(&autoscalingv1.Schedule{}). 143 | Complete(r) 144 | } 145 | -------------------------------------------------------------------------------- /controllers/autoscaling/schedule_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | autoscalingv1 "github.com/d-kuro/scheduled-pod-autoscaler/apis/autoscaling/v1" 9 | "github.com/d-kuro/scheduled-pod-autoscaler/controllers/autoscaling/internal/testutil" 10 | "github.com/onsi/ginkgo" 11 | "github.com/onsi/gomega" 12 | hpav2beta2 "k8s.io/api/autoscaling/v2beta2" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "sigs.k8s.io/controller-runtime/pkg/client" 15 | ) 16 | 17 | var _ = ginkgo.Describe("Schedule controller", func() { 18 | ginkgo.Context("when creating Schedule resource", func() { 19 | ginkgo.It("should set ownerReference", func() { 20 | const ( 21 | name = "schedule-controller-test" 22 | ) 23 | 24 | ctx := context.Background() 25 | now := time.Now().UTC() 26 | spa := newScheduledPodAutoscaler(name) 27 | 28 | // Set a future time and prevent it from being scheduled scaling 29 | start := now.AddDate(0, 0, 1).Format("2006-01-02T15:04") 30 | end := now.AddDate(0, 0, 10).Format("2006-01-02T15:04") 31 | schedule := newSchedule(name, 32 | WithScheduleType(autoscalingv1.OneShot), 33 | WithScheduleStartTime(start), 34 | WithScheduleEndTime(end)) 35 | 36 | err := k8sClient.Create(ctx, spa) 37 | gomega.Expect(err).Should(gomega.Succeed()) 38 | 39 | err = k8sClient.Create(ctx, schedule) 40 | gomega.Expect(err).Should(gomega.Succeed()) 41 | 42 | var createdSchedule autoscalingv1.Schedule 43 | gomega.Eventually(func() error { 44 | if err := k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: defaultTestNamespace}, &createdSchedule); err != nil { 45 | return err 46 | } 47 | 48 | if createdSchedule.Status.Condition != autoscalingv1.ScheduleAvailable { 49 | return fmt.Errorf("schedule condition mismatch: want: %s, got: %s", 50 | autoscalingv1.ScheduleAvailable, createdSchedule.Status.Condition) 51 | } 52 | 53 | if existing := metav1.GetControllerOf(&createdSchedule); existing == nil { 54 | return fmt.Errorf("ownerReference not found") 55 | } 56 | 57 | return nil 58 | }, /*timeout*/ defaultTestTimeout /*pollingInterval*/, defaultTestPollingInterval).Should(gomega.Succeed()) 59 | }) 60 | }) 61 | }) 62 | 63 | const ( 64 | defaultScheduleMinReplicas = 3 65 | defaultScheduleMaxReplicas = 10 66 | defaultScheduleType = "Daily" 67 | defaultScheduleStartTime = "00:00" 68 | defaultScheduleEndTime = "12:00" 69 | ) 70 | 71 | func newSchedule(name string, options ...func(*autoscalingv1.Schedule)) *autoscalingv1.Schedule { 72 | schedule := &autoscalingv1.Schedule{ 73 | TypeMeta: metav1.TypeMeta{ 74 | APIVersion: autoscalingv1.GroupVersion.String(), 75 | Kind: "Schedule", 76 | }, 77 | ObjectMeta: metav1.ObjectMeta{ 78 | Name: name, 79 | Namespace: defaultTestNamespace, 80 | }, 81 | Spec: autoscalingv1.ScheduleSpec{ 82 | ScaleTargetRef: hpav2beta2.CrossVersionObjectReference{ 83 | APIVersion: "autoscaling.d-kuro.github.io/v1", 84 | Kind: "ScheduledPodAutoscaler", 85 | Name: name, 86 | }, 87 | ScheduleType: defaultScheduleType, 88 | MinReplicas: testutil.ToPointerInt32(defaultScheduleMinReplicas), 89 | MaxReplicas: testutil.ToPointerInt32(defaultScheduleMaxReplicas), 90 | StartTime: defaultScheduleStartTime, 91 | EndTime: defaultScheduleEndTime, 92 | }, 93 | } 94 | 95 | for _, option := range options { 96 | option(schedule) 97 | } 98 | 99 | return schedule 100 | } 101 | 102 | func WithScheduleType(value autoscalingv1.ScheduleType) func(*autoscalingv1.Schedule) { 103 | return func(schedule *autoscalingv1.Schedule) { 104 | schedule.Spec.ScheduleType = value 105 | } 106 | } 107 | 108 | func WithScheduleStartTime(t string) func(*autoscalingv1.Schedule) { 109 | return func(schedule *autoscalingv1.Schedule) { 110 | schedule.Spec.StartTime = t 111 | } 112 | } 113 | 114 | func WithScheduleEndTime(t string) func(*autoscalingv1.Schedule) { 115 | return func(schedule *autoscalingv1.Schedule) { 116 | schedule.Spec.EndTime = t 117 | } 118 | } 119 | 120 | func WithScheduleMinReplicas(value int) func(*autoscalingv1.Schedule) { 121 | return func(schedule *autoscalingv1.Schedule) { 122 | schedule.Spec.MinReplicas = testutil.ToPointerInt32(value) 123 | } 124 | } 125 | 126 | func WithScheduleMaxReplicas(value int) func(*autoscalingv1.Schedule) { 127 | return func(schedule *autoscalingv1.Schedule) { 128 | schedule.Spec.MaxReplicas = testutil.ToPointerInt32(value) 129 | } 130 | } 131 | 132 | func WithScheduleSuspend(value bool) func(*autoscalingv1.Schedule) { 133 | return func(schedule *autoscalingv1.Schedule) { 134 | schedule.Spec.Suspend = value 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /controllers/autoscaling/scheduledpodautoscaler_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 | "time" 22 | 23 | autoscalingv1 "github.com/d-kuro/scheduled-pod-autoscaler/apis/autoscaling/v1" 24 | "github.com/go-logr/logr" 25 | hpav2beta2 "k8s.io/api/autoscaling/v2beta2" 26 | corev1 "k8s.io/api/core/v1" 27 | "k8s.io/apimachinery/pkg/api/equality" 28 | apierrors "k8s.io/apimachinery/pkg/api/errors" 29 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | "k8s.io/apimachinery/pkg/runtime" 31 | "k8s.io/client-go/tools/record" 32 | ctrl "sigs.k8s.io/controller-runtime" 33 | "sigs.k8s.io/controller-runtime/pkg/client" 34 | ) 35 | 36 | // ScheduledPodAutoscalerReconciler reconciles a ScheduledPodAutoscaler object. 37 | type ScheduledPodAutoscalerReconciler struct { 38 | client.Client 39 | Log logr.Logger 40 | Scheme *runtime.Scheme 41 | Recorder record.EventRecorder 42 | } 43 | 44 | // +kubebuilder:rbac:groups=autoscaling.d-kuro.github.io,resources=scheduledpodautoscalers,verbs=get;list;watch;create;update;patch;delete 45 | // +kubebuilder:rbac:groups=autoscaling.d-kuro.github.io,resources=scheduledpodautoscalers/status,verbs=get;update;patch 46 | // +kubebuilder:rbac:groups=autoscaling,resources=horizontalpodautoscalers,verbs=get;list;watch;create;update;patch;delete 47 | 48 | func (r *ScheduledPodAutoscalerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 49 | log := r.Log.WithValues("scheduledpodautoscaler", req.NamespacedName) 50 | 51 | var spa autoscalingv1.ScheduledPodAutoscaler 52 | if err := r.Get(ctx, req.NamespacedName, &spa); err != nil { 53 | if apierrors.IsNotFound(err) { 54 | return ctrl.Result{}, client.IgnoreNotFound(err) 55 | } 56 | 57 | log.Error(err, "unable to fetch ScheduledPodAutoscaler") 58 | 59 | return ctrl.Result{}, err 60 | } 61 | 62 | if spa.DeletionTimestamp != nil { 63 | return ctrl.Result{}, nil 64 | } 65 | 66 | var hpa hpav2beta2.HorizontalPodAutoscaler 67 | if err := r.Get(ctx, req.NamespacedName, &hpa); apierrors.IsNotFound(err) { 68 | log.Info("unable to fetch hpa, try to create one", "namespacedName", req.NamespacedName) 69 | 70 | hpa, err = r.createHPA(ctx, log, spa) 71 | if err != nil { 72 | return ctrl.Result{}, err 73 | } 74 | 75 | if err := r.updateScheduledPodAutoscalerStatus(ctx, log, spa, autoscalingv1.ScheduledPodAutoscalerAvailable); err != nil { 76 | log.Error(err, "unable to update ScheduledPodAutoscaler status", "scheduledPodAutoscaler", spa) 77 | } 78 | 79 | log.Info("successfully create HPA", "hpa", hpa) 80 | 81 | if hpa.Spec.MinReplicas != nil { 82 | minReplicasCounter.WithLabelValues(spa.Name, spa.Namespace).Set(float64(*hpa.Spec.MinReplicas)) 83 | } 84 | 85 | maxReplicasCounter.WithLabelValues(spa.Name, spa.Namespace).Set(float64(hpa.Spec.MaxReplicas)) 86 | } else if err != nil { 87 | log.Error(err, "unable to fetch HPA", "namespacedName", req.NamespacedName) 88 | 89 | return ctrl.Result{}, err 90 | } 91 | 92 | updated, err := r.reconcileHPA(ctx, log, spa, hpa) 93 | if err != nil { 94 | log.Error(err, "unable to reconcile") 95 | 96 | return ctrl.Result{}, err 97 | } 98 | 99 | if !updated && !equality.Semantic.DeepEqual(hpa.Spec, spa.Spec.HorizontalPodAutoscalerSpec) { 100 | hpa.Spec = spa.Spec.HorizontalPodAutoscalerSpec 101 | if err := r.Update(ctx, &hpa, &client.UpdateOptions{}); err != nil { 102 | log.Error(err, "unable to update HPA", "hpa", hpa) 103 | 104 | return ctrl.Result{}, err 105 | } 106 | 107 | if err := r.updateScheduledPodAutoscalerStatus(ctx, log, spa, autoscalingv1.ScheduledPodAutoscalerAvailable); err != nil { 108 | log.Error(err, "unable to update ScheduledPodAutoscaler status", "scheduledPodAutoscaler", spa) 109 | } 110 | 111 | log.Info("successfully update HPA", "hpa", hpa) 112 | 113 | if hpa.Spec.MinReplicas != nil { 114 | minReplicasCounter.WithLabelValues(spa.Name, spa.Namespace).Set(float64(*hpa.Spec.MinReplicas)) 115 | } 116 | 117 | maxReplicasCounter.WithLabelValues(spa.Name, spa.Namespace).Set(float64(hpa.Spec.MaxReplicas)) 118 | } 119 | 120 | return ctrl.Result{Requeue: true, RequeueAfter: 30 * time.Second}, nil 121 | } 122 | 123 | func (r *ScheduledPodAutoscalerReconciler) reconcileHPA(ctx context.Context, log logr.Logger, 124 | spa autoscalingv1.ScheduledPodAutoscaler, hpa hpav2beta2.HorizontalPodAutoscaler) (bool, error) { 125 | now := time.Now() 126 | updated := false 127 | var err error 128 | 129 | var schedules autoscalingv1.ScheduleList 130 | if err := r.List(ctx, &schedules, client.MatchingFields(map[string]string{ownerControllerField: spa.Name})); err != nil { 131 | log.Error(err, "unable to list child Schedules", "scheduledPodAutoscaler", spa) 132 | 133 | return updated, err 134 | } 135 | 136 | if len(schedules.Items) == 0 { 137 | log.Info("not found child Schedules", "scheduledPodAutoscaler", spa) 138 | 139 | return updated, nil 140 | } 141 | 142 | var processSchedule []autoscalingv1.Schedule 143 | 144 | for _, schedule := range schedules.Items { 145 | schedule := schedule 146 | 147 | if schedule.Spec.Suspend { 148 | continue 149 | } 150 | 151 | completed, err := schedule.Spec.IsCompleted(now) 152 | if err != nil { 153 | log.Error(err, "unable to check completed Schedule") 154 | 155 | return updated, err 156 | } 157 | 158 | if completed { 159 | if err = r.updateScheduleStatus(ctx, log, schedule, autoscalingv1.ScheduleCompleted); err != nil { 160 | log.Error(err, "unable to update schedule status", "schedule", schedule) 161 | } 162 | 163 | continue 164 | } 165 | 166 | isContains, err := schedule.Spec.Contains(now) 167 | if err != nil { 168 | log.Error(err, "unable to check contains Schedule") 169 | 170 | return updated, err 171 | } 172 | 173 | log.Info("checking included in the schedule scaling period", 174 | "type", schedule.Spec.ScheduleType, 175 | "now", now, 176 | "startTime", schedule.Spec.StartTime, 177 | "endTime", schedule.Spec.EndTime, 178 | "startDayOfWeek", schedule.Spec.StartDayOfWeek, 179 | "endDayOfWeek", schedule.Spec.EndDayOfWeek, 180 | "isContains", isContains, 181 | ) 182 | 183 | if isContains { 184 | processSchedule = append(processSchedule, schedule) 185 | 186 | continue 187 | } 188 | 189 | if err = r.updateScheduleStatus(ctx, log, schedule, autoscalingv1.ScheduleAvailable); err != nil { 190 | log.Error(err, "unable to update schedule status", "schedule", schedule) 191 | } 192 | } 193 | 194 | newMin, newMax := calculateHPAReplica(processSchedule) 195 | newHPA := hpa.DeepCopy() 196 | 197 | if newMin != nil { 198 | newHPA.Spec.MinReplicas = newMin 199 | } 200 | 201 | if newMax != nil { 202 | newHPA.Spec.MaxReplicas = *newMax 203 | } 204 | 205 | if equality.Semantic.DeepEqual(hpa, newHPA) { 206 | return updated, nil 207 | } 208 | 209 | updated, err = r.updateHPA(ctx, log, *newHPA) 210 | if err != nil { 211 | for _, schedule := range processSchedule { 212 | if err = r.updateScheduleStatus(ctx, log, schedule, autoscalingv1.ScheduleDegraded); err != nil { 213 | log.Error(err, "unable to update schedule status", "schedule", schedule) 214 | } 215 | } 216 | 217 | return updated, err 218 | } 219 | 220 | for _, schedule := range processSchedule { 221 | if err := r.updateScheduleStatus(ctx, log, schedule, autoscalingv1.ScheduleProgressing); err != nil { 222 | log.Error(err, "unable to update schedule status", "schedule", schedule) 223 | } 224 | } 225 | 226 | return updated, nil 227 | } 228 | 229 | func (r *ScheduledPodAutoscalerReconciler) createHPA(ctx context.Context, log logr.Logger, 230 | spa autoscalingv1.ScheduledPodAutoscaler) (hpav2beta2.HorizontalPodAutoscaler, error) { 231 | hpa := hpav2beta2.HorizontalPodAutoscaler{ 232 | ObjectMeta: metav1.ObjectMeta{ 233 | Name: spa.Name, 234 | Namespace: spa.Namespace, 235 | }, 236 | Spec: spa.Spec.HorizontalPodAutoscalerSpec, 237 | } 238 | 239 | if err := ctrl.SetControllerReference(&spa, &hpa, r.Scheme); err != nil { 240 | log.Error(err, "unable to set ownerReference", "hpa", hpa) 241 | 242 | return hpav2beta2.HorizontalPodAutoscaler{}, err 243 | } 244 | 245 | if err := r.Create(ctx, &hpa, &client.CreateOptions{}); err != nil { 246 | log.Info("unable to create HPA", "hpa", hpa) 247 | 248 | if err := r.updateScheduledPodAutoscalerStatus(ctx, log, spa, autoscalingv1.ScheduledPodAutoscalerDegraded); err != nil { 249 | log.Error(err, "unable to update ScheduledPodAutoscaler status", "scheduledPodAutoscaler", spa) 250 | } 251 | 252 | return hpav2beta2.HorizontalPodAutoscaler{}, err 253 | } 254 | 255 | return hpa, nil 256 | } 257 | 258 | func (r *ScheduledPodAutoscalerReconciler) updateHPA(ctx context.Context, log logr.Logger, 259 | hpa hpav2beta2.HorizontalPodAutoscaler) (bool, error) { 260 | updated := false 261 | 262 | if err := r.Update(ctx, &hpa, &client.UpdateOptions{}); err != nil { 263 | log.Error(err, "unable to update HPA", "hpa", hpa) 264 | 265 | return updated, err 266 | } 267 | 268 | updated = true 269 | log.Info("successfully update HPA", "hpa", hpa) 270 | 271 | if hpa.Spec.MinReplicas != nil { 272 | minReplicasCounter.WithLabelValues(hpa.Name, hpa.Namespace).Set(float64(*hpa.Spec.MinReplicas)) 273 | } 274 | 275 | maxReplicasCounter.WithLabelValues(hpa.Name, hpa.Namespace).Set(float64(hpa.Spec.MaxReplicas)) 276 | 277 | return updated, nil 278 | } 279 | 280 | func (r *ScheduledPodAutoscalerReconciler) updateScheduleStatus(ctx context.Context, log logr.Logger, 281 | schedule autoscalingv1.Schedule, newCondition autoscalingv1.ScheduleConditionType) error { 282 | if updated := setScheduleCondition(&schedule.Status, newCondition); updated { 283 | r.Recorder.Event(&schedule, corev1.EventTypeNormal, "Updated", "The schedule was updated.") 284 | 285 | if err := r.Status().Update(ctx, &schedule); err != nil { 286 | log.Error(err, "unable to update schedule status", "schedule", schedule) 287 | 288 | return err 289 | } 290 | } 291 | 292 | return nil 293 | } 294 | 295 | func (r *ScheduledPodAutoscalerReconciler) updateScheduledPodAutoscalerStatus(ctx context.Context, log logr.Logger, 296 | spa autoscalingv1.ScheduledPodAutoscaler, newCondition autoscalingv1.ScheduledPodAutoscalerConditionType) error { 297 | if updated := setScheduledPodAutoscalerCondition(&spa.Status, newCondition); updated { 298 | r.Recorder.Event(&spa, corev1.EventTypeNormal, "Updated", "The schedule was updated.") 299 | 300 | if err := r.Status().Update(ctx, &spa); err != nil { 301 | log.Error(err, "unable to update ScheduledPodAutoscaler status", 302 | "scheduledPodAutoscaler", spa) 303 | 304 | return err 305 | } 306 | } 307 | 308 | return nil 309 | } 310 | 311 | // calculateHPAReplica calculates minReplicas and maxReplicas of the HPA from one or more schedules. 312 | // If there is more than one schedule, the maximum value is used for the replicas. 313 | func calculateHPAReplica(schedules []autoscalingv1.Schedule) (minReplicas *int32, maxReplicas *int32) { 314 | var max, min int32 315 | for _, schedule := range schedules { 316 | if schedule.Spec.MinReplicas != nil && *schedule.Spec.MinReplicas > min { 317 | min = *schedule.Spec.MinReplicas 318 | } 319 | 320 | if schedule.Spec.MaxReplicas != nil && *schedule.Spec.MaxReplicas > max { 321 | max = *schedule.Spec.MaxReplicas 322 | } 323 | } 324 | 325 | if min > 0 { 326 | minReplicas = &min 327 | } 328 | 329 | if max > 0 { 330 | maxReplicas = &max 331 | } 332 | 333 | return minReplicas, maxReplicas 334 | } 335 | 336 | func setScheduledPodAutoscalerCondition( 337 | status *autoscalingv1.ScheduledPodAutoscalerStatus, 338 | newCondition autoscalingv1.ScheduledPodAutoscalerConditionType) bool { 339 | updated := false 340 | 341 | if status.Condition == newCondition { 342 | return updated 343 | } 344 | 345 | status.Condition = newCondition 346 | status.LastTransitionTime = metav1.Now() 347 | updated = true 348 | 349 | return updated 350 | } 351 | 352 | const ownerControllerField = ".metadata.controller" 353 | 354 | func indexByOwnerScheduledPodAutoscaler(obj client.Object) []string { 355 | schedule, ok := obj.(*autoscalingv1.Schedule) 356 | if !ok { 357 | return nil 358 | } 359 | 360 | owner := metav1.GetControllerOf(schedule) 361 | if owner == nil { 362 | return nil 363 | } 364 | 365 | if owner.APIVersion != autoscalingv1.GroupVersion.String() || owner.Kind != "ScheduledPodAutoscaler" { 366 | return nil 367 | } 368 | 369 | return []string{owner.Name} 370 | } 371 | 372 | func (r *ScheduledPodAutoscalerReconciler) SetupWithManager(mgr ctrl.Manager) error { 373 | ctx := context.Background() 374 | 375 | err := mgr.GetFieldIndexer(). 376 | IndexField(ctx, &autoscalingv1.Schedule{}, ownerControllerField, indexByOwnerScheduledPodAutoscaler) 377 | if err != nil { 378 | return err 379 | } 380 | 381 | return ctrl.NewControllerManagedBy(mgr). 382 | For(&autoscalingv1.ScheduledPodAutoscaler{}). 383 | Owns(&autoscalingv1.Schedule{}). 384 | Owns(&hpav2beta2.HorizontalPodAutoscaler{}). 385 | Complete(r) 386 | } 387 | -------------------------------------------------------------------------------- /controllers/autoscaling/scheduledpodautoscaler_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | autoscalingv1 "github.com/d-kuro/scheduled-pod-autoscaler/apis/autoscaling/v1" 9 | "github.com/d-kuro/scheduled-pod-autoscaler/controllers/autoscaling/internal/testutil" 10 | "github.com/google/go-cmp/cmp" 11 | "github.com/onsi/ginkgo" 12 | "github.com/onsi/gomega" 13 | hpav2beta2 "k8s.io/api/autoscaling/v2beta2" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | ) 17 | 18 | var _ = ginkgo.Describe("ScheduledPodAutoscaler controller", func() { 19 | ginkgo.Context("when creating ScheduledPodAutoscaler resource", func() { 20 | ginkgo.It("should create HPA", func() { 21 | const ( 22 | name = "create-hpa-test" 23 | ) 24 | 25 | ctx := context.Background() 26 | spa := newScheduledPodAutoscaler(name) 27 | 28 | err := k8sClient.Create(ctx, spa) 29 | gomega.Expect(err).Should(gomega.Succeed()) 30 | 31 | var createdSPA autoscalingv1.ScheduledPodAutoscaler 32 | var createdHPA hpav2beta2.HorizontalPodAutoscaler 33 | 34 | gomega.Eventually(func() error { 35 | if err := k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: defaultTestNamespace}, &createdHPA); err != nil { 36 | return err 37 | } 38 | 39 | if diff := cmp.Diff(spa.Spec.HorizontalPodAutoscalerSpec, createdHPA.Spec); diff != "" { 40 | return fmt.Errorf("created HPA mismatch (-want +got):\\n%s", diff) 41 | } 42 | 43 | if err := k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: defaultTestNamespace}, &createdSPA); err != nil { 44 | return err 45 | } 46 | 47 | if createdSPA.Status.Condition == "" { 48 | return fmt.Errorf("condition not found") 49 | } 50 | 51 | if createdSPA.Status.Condition != autoscalingv1.ScheduledPodAutoscalerAvailable { 52 | return fmt.Errorf("condition not available: %s", createdSPA.Status.Condition) 53 | } 54 | 55 | return nil 56 | }, /*timeout*/ defaultTestTimeout /*pollingInterval*/, defaultTestPollingInterval).Should(gomega.Succeed()) 57 | }) 58 | ginkgo.It("should scaling with daily scheduled scaling", func() { 59 | const ( 60 | name = "scheduled-scaling-test" 61 | scheduledPodAutoscalerMinReplicas = 1 62 | scheduledPodAutoscalerMaxReplicas = 3 63 | scheduleMinReplicas = 5 64 | scheduleMaxReplicas = 10 65 | ) 66 | 67 | ctx := context.Background() 68 | now := time.Now().UTC() 69 | spa := newScheduledPodAutoscaler(name, 70 | WithScheduledPodAutoscalerMinReplicas(scheduledPodAutoscalerMinReplicas), 71 | WithScheduledPodAutoscalerMaxReplicas(scheduledPodAutoscalerMaxReplicas)) 72 | 73 | // Target scheduled scaling with start at the current time and end in one hour 74 | start := now.Format("15:04") 75 | end := now.Add(time.Hour * 1).Format("15:04") 76 | schedule := newSchedule(name, 77 | WithScheduleMinReplicas(scheduleMinReplicas), 78 | WithScheduleMaxReplicas(scheduleMaxReplicas), 79 | WithScheduleType(autoscalingv1.Daily), 80 | WithScheduleStartTime(start), 81 | WithScheduleEndTime(end)) 82 | 83 | err := k8sClient.Create(ctx, spa) 84 | gomega.Expect(err).Should(gomega.Succeed()) 85 | 86 | err = k8sClient.Create(ctx, schedule) 87 | gomega.Expect(err).Should(gomega.Succeed()) 88 | 89 | var createdHPA hpav2beta2.HorizontalPodAutoscaler 90 | var createdSchedule autoscalingv1.Schedule 91 | gomega.Eventually(func() error { 92 | if err := k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: defaultTestNamespace}, &createdHPA); err != nil { 93 | return err 94 | } 95 | 96 | if createdHPA.Spec.MinReplicas == nil { 97 | return fmt.Errorf("created HPA minReplicas mismatch: want: %d, got: nil", scheduleMinReplicas) 98 | } 99 | 100 | if *createdHPA.Spec.MinReplicas != int32(scheduleMinReplicas) { 101 | return fmt.Errorf("created HPA minReplicas mismatch: want: %d, got: %d", 102 | scheduleMinReplicas, *createdHPA.Spec.MinReplicas) 103 | } 104 | 105 | if createdHPA.Spec.MaxReplicas != int32(scheduleMaxReplicas) { 106 | return fmt.Errorf("created HPA maxReplicas mismatch: want: %d, got: %d", 107 | scheduleMaxReplicas, createdHPA.Spec.MaxReplicas) 108 | } 109 | 110 | if err := k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: defaultTestNamespace}, &createdSchedule); err != nil { 111 | return err 112 | } 113 | 114 | if createdSchedule.Status.Condition != autoscalingv1.ScheduleProgressing { 115 | return fmt.Errorf("schedule condition mismatch: want: %s, got: %s", 116 | autoscalingv1.ScheduleProgressing, createdSchedule.Status.Condition) 117 | } 118 | 119 | return nil 120 | }, /*timeout*/ defaultTestTimeout /*pollingInterval*/, defaultTestPollingInterval).Should(gomega.Succeed()) 121 | }) 122 | ginkgo.It("should suspend scheduled scaling", func() { 123 | const ( 124 | name = "scheduled-scaling-suspend-test" 125 | namespace = "default" 126 | scheduledPodAutoscalerMinReplicas = 1 127 | scheduledPodAutoscalerMaxReplicas = 3 128 | scheduleMinReplicas = 5 129 | scheduleMaxReplicas = 10 130 | ) 131 | 132 | ctx := context.Background() 133 | now := time.Now().UTC() 134 | spa := newScheduledPodAutoscaler(name, 135 | WithScheduledPodAutoscalerMinReplicas(scheduledPodAutoscalerMinReplicas), 136 | WithScheduledPodAutoscalerMaxReplicas(scheduledPodAutoscalerMaxReplicas)) 137 | 138 | // Target scheduled scaling with start at the current time and end in one hour 139 | start := now.Format("15:04") 140 | end := now.Add(time.Hour * 1).Format("15:04") 141 | schedule := newSchedule(name, 142 | WithScheduleSuspend(true), // suspend for scheduled scaling 143 | WithScheduleMinReplicas(scheduleMinReplicas), 144 | WithScheduleMaxReplicas(scheduleMaxReplicas), 145 | WithScheduleType(autoscalingv1.Daily), 146 | WithScheduleStartTime(start), 147 | WithScheduleEndTime(end)) 148 | 149 | err := k8sClient.Create(ctx, spa) 150 | gomega.Expect(err).Should(gomega.Succeed()) 151 | 152 | err = k8sClient.Create(ctx, schedule) 153 | gomega.Expect(err).Should(gomega.Succeed()) 154 | 155 | var createdHPA hpav2beta2.HorizontalPodAutoscaler 156 | var createdSchedule autoscalingv1.Schedule 157 | gomega.Eventually(func() error { 158 | if err := k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, &createdHPA); err != nil { 159 | return err 160 | } 161 | 162 | if createdHPA.Spec.MinReplicas == nil { 163 | return fmt.Errorf("created HPA minReplicas mismatch: want: %d, got: nil", scheduleMinReplicas) 164 | } 165 | 166 | if *createdHPA.Spec.MinReplicas != int32(scheduledPodAutoscalerMinReplicas) { 167 | return fmt.Errorf("created HPA minReplicas mismatch: want: %d, got: %d", 168 | scheduledPodAutoscalerMinReplicas, *createdHPA.Spec.MinReplicas) 169 | } 170 | 171 | if createdHPA.Spec.MaxReplicas != int32(scheduledPodAutoscalerMaxReplicas) { 172 | return fmt.Errorf("created HPA maxReplicas mismatch: want: %d, got: %d", 173 | scheduledPodAutoscalerMaxReplicas, createdHPA.Spec.MaxReplicas) 174 | } 175 | 176 | if err := k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, &createdSchedule); err != nil { 177 | return err 178 | } 179 | 180 | if createdSchedule.Status.Condition != autoscalingv1.ScheduleSuspend { 181 | return fmt.Errorf("schedule condition mismatch: want: %s, got: %s", 182 | autoscalingv1.ScheduleSuspend, createdSchedule.Status.Condition) 183 | } 184 | 185 | return nil 186 | }, /*timeout*/ defaultTestTimeout /*pollingInterval*/, defaultTestPollingInterval).Should(gomega.Succeed()) 187 | }) 188 | ginkgo.It("should completed scheduled scaling", func() { 189 | const ( 190 | name = "scheduled-scaling-completed-test" 191 | namespace = "default" 192 | scheduledPodAutoscalerMinReplicas = 1 193 | scheduledPodAutoscalerMaxReplicas = 3 194 | scheduleMinReplicas = 5 195 | scheduleMaxReplicas = 10 196 | ) 197 | 198 | ctx := context.Background() 199 | now := time.Now().UTC() 200 | spa := newScheduledPodAutoscaler(name, 201 | WithScheduledPodAutoscalerMinReplicas(scheduledPodAutoscalerMinReplicas), 202 | WithScheduledPodAutoscalerMaxReplicas(scheduledPodAutoscalerMaxReplicas)) 203 | 204 | // A one-shot schedule with scaling completed 205 | start := now.AddDate(0, 0, -1).Format("2006-01-02T15:04") 206 | end := now.AddDate(0, 0, -10).Format("2006-01-02T15:04") 207 | schedule := newSchedule(name, 208 | WithScheduleType(autoscalingv1.OneShot), 209 | WithScheduleStartTime(start), 210 | WithScheduleEndTime(end)) 211 | 212 | err := k8sClient.Create(ctx, spa) 213 | gomega.Expect(err).Should(gomega.Succeed()) 214 | 215 | err = k8sClient.Create(ctx, schedule) 216 | gomega.Expect(err).Should(gomega.Succeed()) 217 | 218 | var createdHPA hpav2beta2.HorizontalPodAutoscaler 219 | var createdSchedule autoscalingv1.Schedule 220 | gomega.Eventually(func() error { 221 | if err := k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, &createdHPA); err != nil { 222 | return err 223 | } 224 | 225 | if createdHPA.Spec.MinReplicas == nil { 226 | return fmt.Errorf("created HPA minReplicas mismatch: want: %d, got: nil", scheduleMinReplicas) 227 | } 228 | 229 | if *createdHPA.Spec.MinReplicas != int32(scheduledPodAutoscalerMinReplicas) { 230 | return fmt.Errorf("created HPA minReplicas mismatch: want: %d, got: %d", 231 | scheduledPodAutoscalerMinReplicas, *createdHPA.Spec.MinReplicas) 232 | } 233 | 234 | if createdHPA.Spec.MaxReplicas != int32(scheduledPodAutoscalerMaxReplicas) { 235 | return fmt.Errorf("created HPA maxReplicas mismatch: want: %d, got: %d", 236 | scheduledPodAutoscalerMaxReplicas, createdHPA.Spec.MaxReplicas) 237 | } 238 | 239 | if err := k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, &createdSchedule); err != nil { 240 | return err 241 | } 242 | 243 | if createdSchedule.Status.Condition != autoscalingv1.ScheduleCompleted { 244 | return fmt.Errorf("schedule condition mismatch: want: %s, got: %s", 245 | autoscalingv1.ScheduleCompleted, createdSchedule.Status.Condition) 246 | } 247 | 248 | return nil 249 | }, /*timeout*/ defaultTestTimeout /*pollingInterval*/, defaultTestPollingInterval).Should(gomega.Succeed()) 250 | }) 251 | }) 252 | }) 253 | 254 | const ( 255 | defaultSPAMinReplicas = 1 256 | defaultSPAMaxReplicas = 3 257 | ) 258 | 259 | func newScheduledPodAutoscaler(name string, options ...func(*autoscalingv1.ScheduledPodAutoscaler)) *autoscalingv1.ScheduledPodAutoscaler { 260 | spa := &autoscalingv1.ScheduledPodAutoscaler{ 261 | TypeMeta: metav1.TypeMeta{ 262 | APIVersion: autoscalingv1.GroupVersion.String(), 263 | Kind: "ScheduledPodAutoscaler", 264 | }, 265 | ObjectMeta: metav1.ObjectMeta{ 266 | Name: name, 267 | Namespace: defaultTestNamespace, 268 | }, 269 | Spec: autoscalingv1.ScheduledPodAutoscalerSpec{ 270 | HorizontalPodAutoscalerSpec: hpav2beta2.HorizontalPodAutoscalerSpec{ 271 | ScaleTargetRef: hpav2beta2.CrossVersionObjectReference{ 272 | APIVersion: "apps/v1", 273 | Kind: "Deployment", 274 | Name: name, 275 | }, 276 | MinReplicas: testutil.ToPointerInt32(defaultSPAMinReplicas), 277 | MaxReplicas: defaultSPAMaxReplicas, 278 | Metrics: []hpav2beta2.MetricSpec{ 279 | { 280 | Type: "Resource", 281 | Resource: &hpav2beta2.ResourceMetricSource{ 282 | Name: "cpu", 283 | Target: hpav2beta2.MetricTarget{ 284 | Type: "Utilization", 285 | AverageUtilization: testutil.ToPointerInt32(50), 286 | }, 287 | }, 288 | }, 289 | }, 290 | }, 291 | }, 292 | } 293 | 294 | for _, option := range options { 295 | option(spa) 296 | } 297 | 298 | return spa 299 | } 300 | 301 | func WithScheduledPodAutoscalerMinReplicas(value int) func(*autoscalingv1.ScheduledPodAutoscaler) { 302 | return func(spa *autoscalingv1.ScheduledPodAutoscaler) { 303 | spa.Spec.HorizontalPodAutoscalerSpec.MinReplicas = testutil.ToPointerInt32(value) 304 | } 305 | } 306 | 307 | func WithScheduledPodAutoscalerMaxReplicas(value int) func(*autoscalingv1.ScheduledPodAutoscaler) { 308 | return func(spa *autoscalingv1.ScheduledPodAutoscaler) { 309 | spa.Spec.HorizontalPodAutoscalerSpec.MaxReplicas = int32(value) 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /controllers/autoscaling/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 | "path/filepath" 21 | "testing" 22 | "time" 23 | 24 | autoscalingv1 "github.com/d-kuro/scheduled-pod-autoscaler/apis/autoscaling/v1" 25 | . "github.com/onsi/ginkgo" 26 | . "github.com/onsi/gomega" 27 | "k8s.io/client-go/kubernetes/scheme" 28 | "k8s.io/client-go/rest" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | "sigs.k8s.io/controller-runtime/pkg/envtest" 32 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 33 | logf "sigs.k8s.io/controller-runtime/pkg/log" 34 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 35 | ) 36 | 37 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 38 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 39 | 40 | const ( 41 | defaultTestTimeout = time.Second * 10 42 | defaultTestPollingInterval = time.Second * 1 43 | defaultTestNamespace = "default" 44 | ) 45 | 46 | var ( 47 | cfg *rest.Config 48 | k8sClient client.Client 49 | testEnv *envtest.Environment 50 | ) 51 | 52 | func TestAPIs(t *testing.T) { 53 | RegisterFailHandler(Fail) 54 | 55 | RunSpecsWithDefaultAndCustomReporters(t, 56 | "Controller Suite", 57 | []Reporter{printer.NewlineReporter{}}) 58 | } 59 | 60 | var _ = BeforeSuite(func(done Done) { 61 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 62 | 63 | By("bootstrapping test environment") 64 | testEnv = &envtest.Environment{ 65 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, 66 | } 67 | 68 | var err error 69 | cfg, err = testEnv.Start() 70 | Expect(err).ToNot(HaveOccurred()) 71 | Expect(cfg).ToNot(BeNil()) 72 | 73 | err = autoscalingv1.AddToScheme(scheme.Scheme) 74 | Expect(err).NotTo(HaveOccurred()) 75 | 76 | // +kubebuilder:scaffold:scheme 77 | 78 | mgr, err := ctrl.NewManager(cfg, ctrl.Options{ 79 | Scheme: scheme.Scheme, 80 | }) 81 | Expect(err).ToNot(HaveOccurred()) 82 | 83 | err = (&ScheduledPodAutoscalerReconciler{ 84 | Client: mgr.GetClient(), 85 | Log: ctrl.Log.WithName("controllers").WithName("ScheduledPodAutoscaler"), 86 | Scheme: mgr.GetScheme(), 87 | Recorder: mgr.GetEventRecorderFor("scheduledpodautoscaler-controller"), 88 | }).SetupWithManager(mgr) 89 | Expect(err).ToNot(HaveOccurred()) 90 | 91 | err = (&ScheduleReconciler{ 92 | Client: mgr.GetClient(), 93 | Log: ctrl.Log.WithName("controllers").WithName("Schedule"), 94 | Scheme: mgr.GetScheme(), 95 | Recorder: mgr.GetEventRecorderFor("schedule-controller"), 96 | }).SetupWithManager(mgr) 97 | Expect(err).ToNot(HaveOccurred()) 98 | 99 | go func() { 100 | err = mgr.Start(ctrl.SetupSignalHandler()) 101 | Expect(err).ToNot(HaveOccurred()) 102 | }() 103 | 104 | k8sClient = mgr.GetClient() 105 | Expect(k8sClient).ToNot(BeNil()) 106 | 107 | close(done) 108 | }, 60) 109 | 110 | var _ = AfterSuite(func() { 111 | By("tearing down the test environment") 112 | err := testEnv.Stop() 113 | Expect(err).ToNot(HaveOccurred()) 114 | }) 115 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/d-kuro/scheduled-pod-autoscaler 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/go-logr/logr v0.4.0 7 | github.com/google/go-cmp v0.5.6 8 | github.com/onsi/ginkgo v1.16.2 9 | github.com/onsi/gomega v1.13.0 10 | github.com/prometheus/client_golang v1.10.0 11 | k8s.io/api v0.20.2 12 | k8s.io/apimachinery v0.20.2 13 | k8s.io/client-go v0.20.2 14 | sigs.k8s.io/controller-runtime v0.8.3 15 | ) 16 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | 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 | */ -------------------------------------------------------------------------------- /hack/bump-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -o pipefail 4 | 5 | if [ $# -ne 2 ]; then 6 | echo 'please specify two tags, "bump-version.sh "' 7 | exit 1 8 | fi 9 | 10 | # works only with BSD sed 11 | git grep -l -e "$1" --and -e 'd-kuro/scheduled-pod-autoscaler' | xargs -I {} sed -i '' -e "s/$1/$2/g" {} 12 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | 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 | "flag" 21 | "os" 22 | "time" 23 | 24 | autoscalingv1 "github.com/d-kuro/scheduled-pod-autoscaler/apis/autoscaling/v1" 25 | autoscalingcontroller "github.com/d-kuro/scheduled-pod-autoscaler/controllers/autoscaling" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 28 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | "sigs.k8s.io/controller-runtime/pkg/healthz" 31 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 32 | ) 33 | 34 | var ( 35 | scheme = runtime.NewScheme() 36 | setupLog = ctrl.Log.WithName("setup") 37 | ) 38 | 39 | func init() { 40 | _ = clientgoscheme.AddToScheme(scheme) 41 | 42 | _ = autoscalingv1.AddToScheme(scheme) 43 | // +kubebuilder:scaffold:scheme 44 | } 45 | 46 | func main() { 47 | var metricsAddr string 48 | var probeAddr string 49 | var enableLeaderElection bool 50 | syncPeriod := 1 * time.Hour 51 | 52 | opts := zap.Options{} 53 | opts.BindFlags(flag.CommandLine) 54 | 55 | flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 56 | flag.StringVar(&probeAddr, "probe-addr", ":9090", "The address the liveness probe and readiness probe endpoints bind to.") 57 | flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, 58 | "Enable leader election for controller manager. "+ 59 | "Enabling this will ensure there is only one active controller manager.") 60 | flag.Parse() 61 | 62 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 63 | 64 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 65 | Scheme: scheme, 66 | MetricsBindAddress: metricsAddr, 67 | HealthProbeBindAddress: probeAddr, 68 | LivenessEndpointName: "/healthz", 69 | Port: 9443, 70 | SyncPeriod: &syncPeriod, 71 | LeaderElection: enableLeaderElection, 72 | LeaderElectionID: "09d94c38.d-kuro.github.io", 73 | }) 74 | if err != nil { 75 | setupLog.Error(err, "unable to start manager") 76 | os.Exit(1) 77 | } 78 | 79 | if err = mgr.AddHealthzCheck("ping", healthz.Ping); err != nil { 80 | setupLog.Error(err, "unable to add healthz check") 81 | os.Exit(1) 82 | } 83 | 84 | if err = (&autoscalingcontroller.ScheduledPodAutoscalerReconciler{ 85 | Client: mgr.GetClient(), 86 | Log: ctrl.Log.WithName("controllers").WithName("ScheduledPodAutoscaler"), 87 | Scheme: mgr.GetScheme(), 88 | Recorder: mgr.GetEventRecorderFor("scheduledpodautoscaler-controller"), 89 | }).SetupWithManager(mgr); err != nil { 90 | setupLog.Error(err, "unable to create controller", "controller", "ScheduledPodAutoscaler") 91 | os.Exit(1) 92 | } 93 | 94 | if err = (&autoscalingcontroller.ScheduleReconciler{ 95 | Client: mgr.GetClient(), 96 | Log: ctrl.Log.WithName("controllers").WithName("Schedule"), 97 | Scheme: mgr.GetScheme(), 98 | Recorder: mgr.GetEventRecorderFor("schedule-controller"), 99 | }).SetupWithManager(mgr); err != nil { 100 | setupLog.Error(err, "unable to create controller", "controller", "Schedule") 101 | os.Exit(1) 102 | } 103 | 104 | // +kubebuilder:scaffold:builder 105 | 106 | setupLog.Info("starting manager") 107 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 108 | setupLog.Error(err, "problem running manager") 109 | os.Exit(1) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /manifests/crd/autoscaling.d-kuro.github.io_schedules.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.4.1 8 | creationTimestamp: null 9 | name: schedules.autoscaling.d-kuro.github.io 10 | spec: 11 | group: autoscaling.d-kuro.github.io 12 | names: 13 | kind: Schedule 14 | listKind: ScheduleList 15 | plural: schedules 16 | singular: schedule 17 | scope: Namespaced 18 | versions: 19 | - additionalPrinterColumns: 20 | - jsonPath: .spec.scaleTargetRef.name 21 | name: REFERENCE 22 | type: string 23 | - jsonPath: .spec.type 24 | name: TYPE 25 | type: string 26 | - jsonPath: .spec.startTime 27 | name: STARTTIME 28 | type: string 29 | - jsonPath: .spec.endTime 30 | name: ENDTIME 31 | type: string 32 | - jsonPath: .spec.startDayOfWeek 33 | name: STARTDAYOFWEEK 34 | type: string 35 | - jsonPath: .spec.endDayOfWeek 36 | name: ENDDAYOFWEEK 37 | type: string 38 | - jsonPath: .spec.minReplicas 39 | name: MINPODS 40 | priority: 1 41 | type: integer 42 | - jsonPath: .spec.maxReplicas 43 | name: MAXPODS 44 | priority: 1 45 | type: integer 46 | - jsonPath: .status.condition 47 | name: STATUS 48 | type: string 49 | - jsonPath: .metadata.creationTimestamp 50 | name: AGE 51 | type: date 52 | name: v1 53 | schema: 54 | openAPIV3Schema: 55 | description: Schedule is the Schema for the schedules API. 56 | properties: 57 | apiVersion: 58 | description: 'APIVersion defines the versioned schema of this representation 59 | of an object. Servers should convert recognized schemas to the latest 60 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 61 | type: string 62 | kind: 63 | description: 'Kind is a string value representing the REST resource this 64 | object represents. Servers may infer this from the endpoint the client 65 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 66 | type: string 67 | metadata: 68 | type: object 69 | spec: 70 | description: ScheduleSpec defines the desired state of Schedule. 71 | properties: 72 | description: 73 | description: Description is schedule description. 74 | type: string 75 | endDayOfWeek: 76 | description: EndDayOfWeek is scaling end day of week. Represented 77 | by "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", 78 | "Saturday". 79 | enum: 80 | - Sunday 81 | - Monday 82 | - Tuesday 83 | - Wednesday 84 | - Thursday 85 | - Friday 86 | - Saturday 87 | - "" 88 | type: string 89 | endTime: 90 | description: EndTime is scaling end time. Defined in RFC3339 based 91 | format. Different formats are evaluated depending on ScheduleType. 92 | e.g. OneShot(yyyy-MM-ddTHH:mm), Weekly(HH:mm), Daily(HH:mm) 93 | type: string 94 | maxReplicas: 95 | description: MaxReplicas is the upper limit for the number of replicas 96 | to which the autoscaler can scale up. 97 | format: int32 98 | minimum: 1 99 | type: integer 100 | minReplicas: 101 | description: MinReplicas is the lower limit for the number of replicas 102 | to which the autoscaler can scale down. It defaults to 1 pod. 103 | format: int32 104 | minimum: 1 105 | type: integer 106 | scaleTargetRef: 107 | description: ScaleTargetRef points to the target resource to scale, 108 | and is used to the pods for which metrics should be collected, as 109 | well as to actually change the replica count. 110 | properties: 111 | apiVersion: 112 | description: API version of the referent 113 | type: string 114 | kind: 115 | description: 'Kind of the referent; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds"' 116 | type: string 117 | name: 118 | description: 'Name of the referent; More info: http://kubernetes.io/docs/user-guide/identifiers#names' 119 | type: string 120 | required: 121 | - kind 122 | - name 123 | type: object 124 | startDayOfWeek: 125 | description: StartDayOfWeek is scaling start day of week. Represented 126 | by "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", 127 | "Saturday". 128 | enum: 129 | - Sunday 130 | - Monday 131 | - Tuesday 132 | - Wednesday 133 | - Thursday 134 | - Friday 135 | - Saturday 136 | - "" 137 | type: string 138 | startTime: 139 | description: StartTime is scaling start time. Defined in RFC3339 based 140 | format. Different formats are evaluated depending on ScheduleType. 141 | e.g. OneShot(yyyy-MM-ddTHH:mm), Weekly(HH:mm), Daily(HH:mm) 142 | type: string 143 | suspend: 144 | description: Suspend indicates whether to suspend this schedule. (default 145 | is false) 146 | type: boolean 147 | timeZone: 148 | description: TimeZone is the name of the timezone used in the argument 149 | of the time.LoadLocation(name string) function. StartTime and EndTime 150 | are interpreted as the time in the time zone specified by TimeZone. 151 | If not specified, the time will be interpreted as UTC. 152 | type: string 153 | type: 154 | description: ScheduleType is a type of schedule represented by "Weekly", 155 | "Daily", "OneShot". 156 | enum: 157 | - Weekly 158 | - Daily 159 | - OneShot 160 | type: string 161 | required: 162 | - endTime 163 | - scaleTargetRef 164 | - startTime 165 | - type 166 | type: object 167 | status: 168 | description: ScheduleStatus defines the observed state of Schedule. 169 | properties: 170 | condition: 171 | description: Condition is schedule status type. 172 | type: string 173 | lastTransitionTime: 174 | description: LastTransitionTime is the last time the condition transitioned 175 | from one status to another. 176 | format: date-time 177 | type: string 178 | type: object 179 | type: object 180 | served: true 181 | storage: true 182 | subresources: 183 | status: {} 184 | status: 185 | acceptedNames: 186 | kind: "" 187 | plural: "" 188 | conditions: [] 189 | storedVersions: [] 190 | -------------------------------------------------------------------------------- /manifests/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - autoscaling.d-kuro.github.io_scheduledpodautoscalers.yaml 6 | - autoscaling.d-kuro.github.io_schedules.yaml 7 | -------------------------------------------------------------------------------- /manifests/crd/legacy/autoscaling.d-kuro.github.io_scheduledpodautoscalers.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: (devel) 8 | creationTimestamp: null 9 | name: scheduledpodautoscalers.autoscaling.d-kuro.github.io 10 | spec: 11 | additionalPrinterColumns: 12 | - JSONPath: .spec.horizontalPodAutoscalerSpec.minReplicas 13 | name: MINPODS 14 | type: integer 15 | - JSONPath: .spec.horizontalPodAutoscalerSpec.maxReplicas 16 | name: MAXPODS 17 | type: integer 18 | - JSONPath: .status.condition 19 | name: STATUS 20 | type: string 21 | - JSONPath: .metadata.creationTimestamp 22 | name: AGE 23 | type: date 24 | group: autoscaling.d-kuro.github.io 25 | names: 26 | kind: ScheduledPodAutoscaler 27 | listKind: ScheduledPodAutoscalerList 28 | plural: scheduledpodautoscalers 29 | shortNames: 30 | - spa 31 | singular: scheduledpodautoscaler 32 | scope: Namespaced 33 | subresources: 34 | status: {} 35 | validation: 36 | openAPIV3Schema: 37 | description: ScheduledPodAutoscaler is the Schema for the scheduledpodautoscalers 38 | API. 39 | properties: 40 | apiVersion: 41 | description: 'APIVersion defines the versioned schema of this representation 42 | of an object. Servers should convert recognized schemas to the latest 43 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 44 | type: string 45 | kind: 46 | description: 'Kind is a string value representing the REST resource this 47 | object represents. Servers may infer this from the endpoint the client 48 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 49 | type: string 50 | metadata: 51 | type: object 52 | spec: 53 | description: ScheduledPodAutoscalerSpec defines the desired state of ScheduledPodAutoscaler. 54 | properties: 55 | horizontalPodAutoscalerSpec: 56 | description: 'HorizontalPodAutoscalerSpec is HorizontalPodAutoscaler 57 | v2beta2 API spec. ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#horizontalpodautoscaler-v2beta2-autoscaling' 58 | properties: 59 | behavior: 60 | description: behavior configures the scaling behavior of the target 61 | in both Up and Down directions (scaleUp and scaleDown fields respectively). 62 | If not set, the default HPAScalingRules for scale up and scale 63 | down are used. 64 | properties: 65 | scaleDown: 66 | description: scaleDown is scaling policy for scaling Down. If 67 | not set, the default value is to allow to scale down to minReplicas 68 | pods, with a 300 second stabilization window (i.e., the highest 69 | recommendation for the last 300sec is used). 70 | properties: 71 | policies: 72 | description: policies is a list of potential scaling polices 73 | which can be used during scaling. At least one policy 74 | must be specified, otherwise the HPAScalingRules will 75 | be discarded as invalid 76 | items: 77 | description: HPAScalingPolicy is a single policy which 78 | must hold true for a specified past interval. 79 | properties: 80 | periodSeconds: 81 | description: PeriodSeconds specifies the window of 82 | time for which the policy should hold true. PeriodSeconds 83 | must be greater than zero and less than or equal 84 | to 1800 (30 min). 85 | format: int32 86 | type: integer 87 | type: 88 | description: Type is used to specify the scaling policy. 89 | type: string 90 | value: 91 | description: Value contains the amount of change which 92 | is permitted by the policy. It must be greater than 93 | zero 94 | format: int32 95 | type: integer 96 | required: 97 | - periodSeconds 98 | - type 99 | - value 100 | type: object 101 | type: array 102 | selectPolicy: 103 | description: selectPolicy is used to specify which policy 104 | should be used. If not set, the default value MaxPolicySelect 105 | is used. 106 | type: string 107 | stabilizationWindowSeconds: 108 | description: 'StabilizationWindowSeconds is the number of 109 | seconds for which past recommendations should be considered 110 | while scaling up or scaling down. StabilizationWindowSeconds 111 | must be greater than or equal to zero and less than or 112 | equal to 3600 (one hour). If not set, use the default 113 | values: - For scale up: 0 (i.e. no stabilization is done). 114 | - For scale down: 300 (i.e. the stabilization window is 115 | 300 seconds long).' 116 | format: int32 117 | type: integer 118 | type: object 119 | scaleUp: 120 | description: 'scaleUp is scaling policy for scaling Up. If not 121 | set, the default value is the higher of: * increase no more 122 | than 4 pods per 60 seconds * double the number of pods per 123 | 60 seconds No stabilization is used.' 124 | properties: 125 | policies: 126 | description: policies is a list of potential scaling polices 127 | which can be used during scaling. At least one policy 128 | must be specified, otherwise the HPAScalingRules will 129 | be discarded as invalid 130 | items: 131 | description: HPAScalingPolicy is a single policy which 132 | must hold true for a specified past interval. 133 | properties: 134 | periodSeconds: 135 | description: PeriodSeconds specifies the window of 136 | time for which the policy should hold true. PeriodSeconds 137 | must be greater than zero and less than or equal 138 | to 1800 (30 min). 139 | format: int32 140 | type: integer 141 | type: 142 | description: Type is used to specify the scaling policy. 143 | type: string 144 | value: 145 | description: Value contains the amount of change which 146 | is permitted by the policy. It must be greater than 147 | zero 148 | format: int32 149 | type: integer 150 | required: 151 | - periodSeconds 152 | - type 153 | - value 154 | type: object 155 | type: array 156 | selectPolicy: 157 | description: selectPolicy is used to specify which policy 158 | should be used. If not set, the default value MaxPolicySelect 159 | is used. 160 | type: string 161 | stabilizationWindowSeconds: 162 | description: 'StabilizationWindowSeconds is the number of 163 | seconds for which past recommendations should be considered 164 | while scaling up or scaling down. StabilizationWindowSeconds 165 | must be greater than or equal to zero and less than or 166 | equal to 3600 (one hour). If not set, use the default 167 | values: - For scale up: 0 (i.e. no stabilization is done). 168 | - For scale down: 300 (i.e. the stabilization window is 169 | 300 seconds long).' 170 | format: int32 171 | type: integer 172 | type: object 173 | type: object 174 | maxReplicas: 175 | description: maxReplicas is the upper limit for the number of replicas 176 | to which the autoscaler can scale up. It cannot be less that minReplicas. 177 | format: int32 178 | type: integer 179 | metrics: 180 | description: metrics contains the specifications for which to use 181 | to calculate the desired replica count (the maximum replica count 182 | across all metrics will be used). The desired replica count is 183 | calculated multiplying the ratio between the target value and 184 | the current value by the current number of pods. Ergo, metrics 185 | used must decrease as the pod count is increased, and vice-versa. See 186 | the individual metric source types for more information about 187 | how each type of metric must respond. If not set, the default 188 | metric will be set to 80% average CPU utilization. 189 | items: 190 | description: MetricSpec specifies how to scale based on a single 191 | metric (only `type` and one other matching field should be set 192 | at once). 193 | properties: 194 | containerResource: 195 | description: container resource refers to a resource metric 196 | (such as those specified in requests and limits) known to 197 | Kubernetes describing a single container in each pod of 198 | the current scale target (e.g. CPU or memory). Such metrics 199 | are built in to Kubernetes, and have special scaling options 200 | on top of those available to normal per-pod metrics using 201 | the "pods" source. This is an alpha feature and can be enabled 202 | by the HPAContainerMetrics feature flag. 203 | properties: 204 | container: 205 | description: container is the name of the container in 206 | the pods of the scaling target 207 | type: string 208 | name: 209 | description: name is the name of the resource in question. 210 | type: string 211 | target: 212 | description: target specifies the target value for the 213 | given metric 214 | properties: 215 | averageUtilization: 216 | description: averageUtilization is the target value 217 | of the average of the resource metric across all 218 | relevant pods, represented as a percentage of the 219 | requested value of the resource for the pods. Currently 220 | only valid for Resource metric source type 221 | format: int32 222 | type: integer 223 | averageValue: 224 | anyOf: 225 | - type: integer 226 | - type: string 227 | description: averageValue is the target value of the 228 | average of the metric across all relevant pods (as 229 | a quantity) 230 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 231 | x-kubernetes-int-or-string: true 232 | type: 233 | description: type represents whether the metric type 234 | is Utilization, Value, or AverageValue 235 | type: string 236 | value: 237 | anyOf: 238 | - type: integer 239 | - type: string 240 | description: value is the target value of the metric 241 | (as a quantity). 242 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 243 | x-kubernetes-int-or-string: true 244 | required: 245 | - type 246 | type: object 247 | required: 248 | - container 249 | - name 250 | - target 251 | type: object 252 | external: 253 | description: external refers to a global metric that is not 254 | associated with any Kubernetes object. It allows autoscaling 255 | based on information coming from components running outside 256 | of cluster (for example length of queue in cloud messaging 257 | service, or QPS from loadbalancer running outside of cluster). 258 | properties: 259 | metric: 260 | description: metric identifies the target metric by name 261 | and selector 262 | properties: 263 | name: 264 | description: name is the name of the given metric 265 | type: string 266 | selector: 267 | description: selector is the string-encoded form of 268 | a standard kubernetes label selector for the given 269 | metric When set, it is passed as an additional parameter 270 | to the metrics server for more specific metrics 271 | scoping. When unset, just the metricName will be 272 | used to gather metrics. 273 | properties: 274 | matchExpressions: 275 | description: matchExpressions is a list of label 276 | selector requirements. The requirements are 277 | ANDed. 278 | items: 279 | description: A label selector requirement is 280 | a selector that contains values, a key, and 281 | an operator that relates the key and values. 282 | properties: 283 | key: 284 | description: key is the label key that the 285 | selector applies to. 286 | type: string 287 | operator: 288 | description: operator represents a key's 289 | relationship to a set of values. Valid 290 | operators are In, NotIn, Exists and DoesNotExist. 291 | type: string 292 | values: 293 | description: values is an array of string 294 | values. If the operator is In or NotIn, 295 | the values array must be non-empty. If 296 | the operator is Exists or DoesNotExist, 297 | the values array must be empty. This array 298 | is replaced during a strategic merge patch. 299 | items: 300 | type: string 301 | type: array 302 | required: 303 | - key 304 | - operator 305 | type: object 306 | type: array 307 | matchLabels: 308 | additionalProperties: 309 | type: string 310 | description: matchLabels is a map of {key,value} 311 | pairs. A single {key,value} in the matchLabels 312 | map is equivalent to an element of matchExpressions, 313 | whose key field is "key", the operator is "In", 314 | and the values array contains only "value". 315 | The requirements are ANDed. 316 | type: object 317 | type: object 318 | required: 319 | - name 320 | type: object 321 | target: 322 | description: target specifies the target value for the 323 | given metric 324 | properties: 325 | averageUtilization: 326 | description: averageUtilization is the target value 327 | of the average of the resource metric across all 328 | relevant pods, represented as a percentage of the 329 | requested value of the resource for the pods. Currently 330 | only valid for Resource metric source type 331 | format: int32 332 | type: integer 333 | averageValue: 334 | anyOf: 335 | - type: integer 336 | - type: string 337 | description: averageValue is the target value of the 338 | average of the metric across all relevant pods (as 339 | a quantity) 340 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 341 | x-kubernetes-int-or-string: true 342 | type: 343 | description: type represents whether the metric type 344 | is Utilization, Value, or AverageValue 345 | type: string 346 | value: 347 | anyOf: 348 | - type: integer 349 | - type: string 350 | description: value is the target value of the metric 351 | (as a quantity). 352 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 353 | x-kubernetes-int-or-string: true 354 | required: 355 | - type 356 | type: object 357 | required: 358 | - metric 359 | - target 360 | type: object 361 | object: 362 | description: object refers to a metric describing a single 363 | kubernetes object (for example, hits-per-second on an Ingress 364 | object). 365 | properties: 366 | describedObject: 367 | description: CrossVersionObjectReference contains enough 368 | information to let you identify the referred resource. 369 | properties: 370 | apiVersion: 371 | description: API version of the referent 372 | type: string 373 | kind: 374 | description: 'Kind of the referent; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds"' 375 | type: string 376 | name: 377 | description: 'Name of the referent; More info: http://kubernetes.io/docs/user-guide/identifiers#names' 378 | type: string 379 | required: 380 | - kind 381 | - name 382 | type: object 383 | metric: 384 | description: metric identifies the target metric by name 385 | and selector 386 | properties: 387 | name: 388 | description: name is the name of the given metric 389 | type: string 390 | selector: 391 | description: selector is the string-encoded form of 392 | a standard kubernetes label selector for the given 393 | metric When set, it is passed as an additional parameter 394 | to the metrics server for more specific metrics 395 | scoping. When unset, just the metricName will be 396 | used to gather metrics. 397 | properties: 398 | matchExpressions: 399 | description: matchExpressions is a list of label 400 | selector requirements. The requirements are 401 | ANDed. 402 | items: 403 | description: A label selector requirement is 404 | a selector that contains values, a key, and 405 | an operator that relates the key and values. 406 | properties: 407 | key: 408 | description: key is the label key that the 409 | selector applies to. 410 | type: string 411 | operator: 412 | description: operator represents a key's 413 | relationship to a set of values. Valid 414 | operators are In, NotIn, Exists and DoesNotExist. 415 | type: string 416 | values: 417 | description: values is an array of string 418 | values. If the operator is In or NotIn, 419 | the values array must be non-empty. If 420 | the operator is Exists or DoesNotExist, 421 | the values array must be empty. This array 422 | is replaced during a strategic merge patch. 423 | items: 424 | type: string 425 | type: array 426 | required: 427 | - key 428 | - operator 429 | type: object 430 | type: array 431 | matchLabels: 432 | additionalProperties: 433 | type: string 434 | description: matchLabels is a map of {key,value} 435 | pairs. A single {key,value} in the matchLabels 436 | map is equivalent to an element of matchExpressions, 437 | whose key field is "key", the operator is "In", 438 | and the values array contains only "value". 439 | The requirements are ANDed. 440 | type: object 441 | type: object 442 | required: 443 | - name 444 | type: object 445 | target: 446 | description: target specifies the target value for the 447 | given metric 448 | properties: 449 | averageUtilization: 450 | description: averageUtilization is the target value 451 | of the average of the resource metric across all 452 | relevant pods, represented as a percentage of the 453 | requested value of the resource for the pods. Currently 454 | only valid for Resource metric source type 455 | format: int32 456 | type: integer 457 | averageValue: 458 | anyOf: 459 | - type: integer 460 | - type: string 461 | description: averageValue is the target value of the 462 | average of the metric across all relevant pods (as 463 | a quantity) 464 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 465 | x-kubernetes-int-or-string: true 466 | type: 467 | description: type represents whether the metric type 468 | is Utilization, Value, or AverageValue 469 | type: string 470 | value: 471 | anyOf: 472 | - type: integer 473 | - type: string 474 | description: value is the target value of the metric 475 | (as a quantity). 476 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 477 | x-kubernetes-int-or-string: true 478 | required: 479 | - type 480 | type: object 481 | required: 482 | - describedObject 483 | - metric 484 | - target 485 | type: object 486 | pods: 487 | description: pods refers to a metric describing each pod in 488 | the current scale target (for example, transactions-processed-per-second). The 489 | values will be averaged together before being compared to 490 | the target value. 491 | properties: 492 | metric: 493 | description: metric identifies the target metric by name 494 | and selector 495 | properties: 496 | name: 497 | description: name is the name of the given metric 498 | type: string 499 | selector: 500 | description: selector is the string-encoded form of 501 | a standard kubernetes label selector for the given 502 | metric When set, it is passed as an additional parameter 503 | to the metrics server for more specific metrics 504 | scoping. When unset, just the metricName will be 505 | used to gather metrics. 506 | properties: 507 | matchExpressions: 508 | description: matchExpressions is a list of label 509 | selector requirements. The requirements are 510 | ANDed. 511 | items: 512 | description: A label selector requirement is 513 | a selector that contains values, a key, and 514 | an operator that relates the key and values. 515 | properties: 516 | key: 517 | description: key is the label key that the 518 | selector applies to. 519 | type: string 520 | operator: 521 | description: operator represents a key's 522 | relationship to a set of values. Valid 523 | operators are In, NotIn, Exists and DoesNotExist. 524 | type: string 525 | values: 526 | description: values is an array of string 527 | values. If the operator is In or NotIn, 528 | the values array must be non-empty. If 529 | the operator is Exists or DoesNotExist, 530 | the values array must be empty. This array 531 | is replaced during a strategic merge patch. 532 | items: 533 | type: string 534 | type: array 535 | required: 536 | - key 537 | - operator 538 | type: object 539 | type: array 540 | matchLabels: 541 | additionalProperties: 542 | type: string 543 | description: matchLabels is a map of {key,value} 544 | pairs. A single {key,value} in the matchLabels 545 | map is equivalent to an element of matchExpressions, 546 | whose key field is "key", the operator is "In", 547 | and the values array contains only "value". 548 | The requirements are ANDed. 549 | type: object 550 | type: object 551 | required: 552 | - name 553 | type: object 554 | target: 555 | description: target specifies the target value for the 556 | given metric 557 | properties: 558 | averageUtilization: 559 | description: averageUtilization is the target value 560 | of the average of the resource metric across all 561 | relevant pods, represented as a percentage of the 562 | requested value of the resource for the pods. Currently 563 | only valid for Resource metric source type 564 | format: int32 565 | type: integer 566 | averageValue: 567 | anyOf: 568 | - type: integer 569 | - type: string 570 | description: averageValue is the target value of the 571 | average of the metric across all relevant pods (as 572 | a quantity) 573 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 574 | x-kubernetes-int-or-string: true 575 | type: 576 | description: type represents whether the metric type 577 | is Utilization, Value, or AverageValue 578 | type: string 579 | value: 580 | anyOf: 581 | - type: integer 582 | - type: string 583 | description: value is the target value of the metric 584 | (as a quantity). 585 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 586 | x-kubernetes-int-or-string: true 587 | required: 588 | - type 589 | type: object 590 | required: 591 | - metric 592 | - target 593 | type: object 594 | resource: 595 | description: resource refers to a resource metric (such as 596 | those specified in requests and limits) known to Kubernetes 597 | describing each pod in the current scale target (e.g. CPU 598 | or memory). Such metrics are built in to Kubernetes, and 599 | have special scaling options on top of those available to 600 | normal per-pod metrics using the "pods" source. 601 | properties: 602 | name: 603 | description: name is the name of the resource in question. 604 | type: string 605 | target: 606 | description: target specifies the target value for the 607 | given metric 608 | properties: 609 | averageUtilization: 610 | description: averageUtilization is the target value 611 | of the average of the resource metric across all 612 | relevant pods, represented as a percentage of the 613 | requested value of the resource for the pods. Currently 614 | only valid for Resource metric source type 615 | format: int32 616 | type: integer 617 | averageValue: 618 | anyOf: 619 | - type: integer 620 | - type: string 621 | description: averageValue is the target value of the 622 | average of the metric across all relevant pods (as 623 | a quantity) 624 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 625 | x-kubernetes-int-or-string: true 626 | type: 627 | description: type represents whether the metric type 628 | is Utilization, Value, or AverageValue 629 | type: string 630 | value: 631 | anyOf: 632 | - type: integer 633 | - type: string 634 | description: value is the target value of the metric 635 | (as a quantity). 636 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 637 | x-kubernetes-int-or-string: true 638 | required: 639 | - type 640 | type: object 641 | required: 642 | - name 643 | - target 644 | type: object 645 | type: 646 | description: 'type is the type of metric source. It should 647 | be one of "ContainerResource", "External", "Object", "Pods" 648 | or "Resource", each mapping to a matching field in the object. 649 | Note: "ContainerResource" type is available on when the 650 | feature-gate HPAContainerMetrics is enabled' 651 | type: string 652 | required: 653 | - type 654 | type: object 655 | type: array 656 | minReplicas: 657 | description: minReplicas is the lower limit for the number of replicas 658 | to which the autoscaler can scale down. It defaults to 1 pod. minReplicas 659 | is allowed to be 0 if the alpha feature gate HPAScaleToZero is 660 | enabled and at least one Object or External metric is configured. Scaling 661 | is active as long as at least one metric value is available. 662 | format: int32 663 | type: integer 664 | scaleTargetRef: 665 | description: scaleTargetRef points to the target resource to scale, 666 | and is used to the pods for which metrics should be collected, 667 | as well as to actually change the replica count. 668 | properties: 669 | apiVersion: 670 | description: API version of the referent 671 | type: string 672 | kind: 673 | description: 'Kind of the referent; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds"' 674 | type: string 675 | name: 676 | description: 'Name of the referent; More info: http://kubernetes.io/docs/user-guide/identifiers#names' 677 | type: string 678 | required: 679 | - kind 680 | - name 681 | type: object 682 | required: 683 | - maxReplicas 684 | - scaleTargetRef 685 | type: object 686 | required: 687 | - horizontalPodAutoscalerSpec 688 | type: object 689 | status: 690 | description: ScheduledPodAutoscalerStatus defines the observed state of 691 | ScheduledPodAutoscaler. 692 | properties: 693 | condition: 694 | description: Condition is schedule status type. 695 | type: string 696 | lastTransitionTime: 697 | description: LastTransitionTime is the last time the condition transitioned 698 | from one status to another. 699 | format: date-time 700 | type: string 701 | type: object 702 | type: object 703 | version: v1 704 | versions: 705 | - name: v1 706 | served: true 707 | storage: true 708 | status: 709 | acceptedNames: 710 | kind: "" 711 | plural: "" 712 | conditions: [] 713 | storedVersions: [] 714 | -------------------------------------------------------------------------------- /manifests/crd/legacy/autoscaling.d-kuro.github.io_schedules.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: (devel) 8 | creationTimestamp: null 9 | name: schedules.autoscaling.d-kuro.github.io 10 | spec: 11 | additionalPrinterColumns: 12 | - JSONPath: .spec.scaleTargetRef.name 13 | name: REFERENCE 14 | type: string 15 | - JSONPath: .spec.type 16 | name: TYPE 17 | type: string 18 | - JSONPath: .spec.startTime 19 | name: STARTTIME 20 | type: string 21 | - JSONPath: .spec.endTime 22 | name: ENDTIME 23 | type: string 24 | - JSONPath: .spec.startDayOfWeek 25 | name: STARTDAYOFWEEK 26 | type: string 27 | - JSONPath: .spec.endDayOfWeek 28 | name: ENDDAYOFWEEK 29 | type: string 30 | - JSONPath: .spec.minReplicas 31 | name: MINPODS 32 | priority: 1 33 | type: integer 34 | - JSONPath: .spec.maxReplicas 35 | name: MAXPODS 36 | priority: 1 37 | type: integer 38 | - JSONPath: .status.condition 39 | name: STATUS 40 | type: string 41 | - JSONPath: .metadata.creationTimestamp 42 | name: AGE 43 | type: date 44 | group: autoscaling.d-kuro.github.io 45 | names: 46 | kind: Schedule 47 | listKind: ScheduleList 48 | plural: schedules 49 | singular: schedule 50 | scope: Namespaced 51 | subresources: 52 | status: {} 53 | validation: 54 | openAPIV3Schema: 55 | description: Schedule is the Schema for the schedules API. 56 | properties: 57 | apiVersion: 58 | description: 'APIVersion defines the versioned schema of this representation 59 | of an object. Servers should convert recognized schemas to the latest 60 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 61 | type: string 62 | kind: 63 | description: 'Kind is a string value representing the REST resource this 64 | object represents. Servers may infer this from the endpoint the client 65 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 66 | type: string 67 | metadata: 68 | type: object 69 | spec: 70 | description: ScheduleSpec defines the desired state of Schedule. 71 | properties: 72 | description: 73 | description: Description is schedule description. 74 | type: string 75 | endDayOfWeek: 76 | description: EndDayOfWeek is scaling end day of week. Represented by 77 | "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", 78 | "Saturday". 79 | enum: 80 | - Sunday 81 | - Monday 82 | - Tuesday 83 | - Wednesday 84 | - Thursday 85 | - Friday 86 | - Saturday 87 | - "" 88 | type: string 89 | endTime: 90 | description: EndTime is scaling end time. Defined in RFC3339 based format. 91 | Different formats are evaluated depending on ScheduleType. e.g. OneShot(yyyy-MM-ddTHH:mm), 92 | Weekly(HH:mm), Daily(HH:mm) 93 | type: string 94 | maxReplicas: 95 | description: MaxReplicas is the upper limit for the number of replicas 96 | to which the autoscaler can scale up. 97 | format: int32 98 | minimum: 1 99 | type: integer 100 | minReplicas: 101 | description: MinReplicas is the lower limit for the number of replicas 102 | to which the autoscaler can scale down. It defaults to 1 pod. 103 | format: int32 104 | minimum: 1 105 | type: integer 106 | scaleTargetRef: 107 | description: ScaleTargetRef points to the target resource to scale, 108 | and is used to the pods for which metrics should be collected, as 109 | well as to actually change the replica count. 110 | properties: 111 | apiVersion: 112 | description: API version of the referent 113 | type: string 114 | kind: 115 | description: 'Kind of the referent; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds"' 116 | type: string 117 | name: 118 | description: 'Name of the referent; More info: http://kubernetes.io/docs/user-guide/identifiers#names' 119 | type: string 120 | required: 121 | - kind 122 | - name 123 | type: object 124 | startDayOfWeek: 125 | description: StartDayOfWeek is scaling start day of week. Represented 126 | by "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", 127 | "Saturday". 128 | enum: 129 | - Sunday 130 | - Monday 131 | - Tuesday 132 | - Wednesday 133 | - Thursday 134 | - Friday 135 | - Saturday 136 | - "" 137 | type: string 138 | startTime: 139 | description: StartTime is scaling start time. Defined in RFC3339 based 140 | format. Different formats are evaluated depending on ScheduleType. 141 | e.g. OneShot(yyyy-MM-ddTHH:mm), Weekly(HH:mm), Daily(HH:mm) 142 | type: string 143 | suspend: 144 | description: Suspend indicates whether to suspend this schedule. (default 145 | is false) 146 | type: boolean 147 | timeZone: 148 | description: TimeZone is the name of the timezone used in the argument 149 | of the time.LoadLocation(name string) function. StartTime and EndTime 150 | are interpreted as the time in the time zone specified by TimeZone. 151 | If not specified, the time will be interpreted as UTC. 152 | type: string 153 | type: 154 | description: ScheduleType is a type of schedule represented by "Weekly", 155 | "Daily", "OneShot". 156 | enum: 157 | - Weekly 158 | - Daily 159 | - OneShot 160 | type: string 161 | required: 162 | - endTime 163 | - scaleTargetRef 164 | - startTime 165 | - type 166 | type: object 167 | status: 168 | description: ScheduleStatus defines the observed state of Schedule. 169 | properties: 170 | condition: 171 | description: Condition is schedule status type. 172 | type: string 173 | lastTransitionTime: 174 | description: LastTransitionTime is the last time the condition transitioned 175 | from one status to another. 176 | format: date-time 177 | type: string 178 | type: object 179 | type: object 180 | version: v1 181 | versions: 182 | - name: v1 183 | served: true 184 | storage: true 185 | status: 186 | acceptedNames: 187 | kind: "" 188 | plural: "" 189 | conditions: [] 190 | storedVersions: [] 191 | -------------------------------------------------------------------------------- /manifests/crd/legacy/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - autoscaling.d-kuro.github.io_scheduledpodautoscalers.yaml 6 | - autoscaling.d-kuro.github.io_schedules.yaml 7 | -------------------------------------------------------------------------------- /manifests/deployment/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - scheduled-pod-autoscaler.yaml 6 | -------------------------------------------------------------------------------- /manifests/deployment/scheduled-pod-autoscaler.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: scheduled-pod-autoscaler 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app.kubernetes.io/name: scheduled-pod-autoscaler 10 | template: 11 | metadata: 12 | labels: 13 | app.kubernetes.io/name: scheduled-pod-autoscaler 14 | spec: 15 | containers: 16 | - command: 17 | - /manager 18 | args: 19 | - --enable-leader-election 20 | image: ghcr.io/d-kuro/scheduled-pod-autoscaler:v0.0.3 21 | name: manager 22 | ports: 23 | - containerPort: 8080 24 | - containerPort: 9090 25 | resources: 26 | limits: 27 | memory: 100Mi 28 | requests: 29 | cpu: 100m 30 | memory: 100Mi 31 | livenessProbe: 32 | httpGet: 33 | path: /healthz 34 | port: 9090 35 | serviceAccountName: scheduled-pod-autoscaler 36 | -------------------------------------------------------------------------------- /manifests/install/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namespace: kube-system 5 | 6 | resources: 7 | - ../crd 8 | - ../deployment 9 | - ../rbac 10 | -------------------------------------------------------------------------------- /manifests/install/legacy/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namespace: kube-system 5 | 6 | resources: 7 | - ../../crd/legacy 8 | - ../../deployment 9 | - ../../rbac 10 | -------------------------------------------------------------------------------- /manifests/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - leader_election_role.yaml 6 | - leader_election_role_binding.yaml 7 | - role.yaml 8 | - role_binding.yaml 9 | - service_account.yaml 10 | -------------------------------------------------------------------------------- /manifests/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: scheduled-pod-autoscaler-leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | - coordination.k8s.io 10 | resources: 11 | - configmaps 12 | - leases 13 | verbs: 14 | - get 15 | - list 16 | - watch 17 | - create 18 | - update 19 | - patch 20 | - delete 21 | - apiGroups: 22 | - "" 23 | resources: 24 | - configmaps/status 25 | verbs: 26 | - get 27 | - update 28 | - patch 29 | - apiGroups: 30 | - "" 31 | resources: 32 | - events 33 | verbs: 34 | - create 35 | -------------------------------------------------------------------------------- /manifests/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: scheduled-pod-autoscaler-leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: scheduled-pod-autoscaler-leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: scheduled-pod-autoscaler 12 | namespace: kube-system 13 | -------------------------------------------------------------------------------- /manifests/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | creationTimestamp: null 7 | name: scheduled-pod-autoscaler-role 8 | rules: 9 | - apiGroups: 10 | - autoscaling 11 | resources: 12 | - horizontalpodautoscalers 13 | verbs: 14 | - create 15 | - delete 16 | - get 17 | - list 18 | - patch 19 | - update 20 | - watch 21 | - apiGroups: 22 | - autoscaling.d-kuro.github.io 23 | resources: 24 | - scheduledpodautoscalers 25 | verbs: 26 | - create 27 | - delete 28 | - get 29 | - list 30 | - patch 31 | - update 32 | - watch 33 | - apiGroups: 34 | - autoscaling.d-kuro.github.io 35 | resources: 36 | - scheduledpodautoscalers/status 37 | verbs: 38 | - get 39 | - patch 40 | - update 41 | - apiGroups: 42 | - autoscaling.d-kuro.github.io 43 | resources: 44 | - schedules 45 | verbs: 46 | - create 47 | - delete 48 | - get 49 | - list 50 | - patch 51 | - update 52 | - watch 53 | - apiGroups: 54 | - autoscaling.d-kuro.github.io 55 | resources: 56 | - schedules/status 57 | verbs: 58 | - get 59 | - patch 60 | - update 61 | -------------------------------------------------------------------------------- /manifests/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: scheduled-pod-autoscaler-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: scheduled-pod-autoscaler-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: scheduled-pod-autoscaler 12 | namespace: kube-system 13 | -------------------------------------------------------------------------------- /manifests/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: scheduled-pod-autoscaler 5 | --------------------------------------------------------------------------------