├── .github └── workflows │ ├── merge.yaml │ ├── pull-request.yaml │ └── release.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── OWNERS ├── PROJECT ├── README.md ├── apis ├── meta │ └── v1alpha1 │ │ ├── groupversion_info.go │ │ ├── meta.go │ │ ├── status_helpers.go │ │ ├── status_types.go │ │ └── zz_generated.deepcopy.go └── rdb │ └── v1alpha1 │ ├── groupversion_info.go │ ├── rdbdatabase_types.go │ ├── rdbinstance_types.go │ ├── rdbuser_types.go │ └── zz_generated.deepcopy.go ├── config ├── certmanager │ ├── certificate.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── crd │ ├── bases │ │ ├── rdb.scaleway.com_rdbdatabases.yaml │ │ ├── rdb.scaleway.com_rdbinstances.yaml │ │ └── rdb.scaleway.com_rdbusers.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_rdbdatabases.yaml │ │ ├── cainjection_in_rdbinstances.yaml │ │ ├── cainjection_in_rdbusers.yaml │ │ ├── webhook_in_rdbdatabases.yaml │ │ ├── webhook_in_rdbinstances.yaml │ │ └── webhook_in_rdbusers.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 │ ├── rdbdatabase_editor_role.yaml │ ├── rdbdatabase_viewer_role.yaml │ ├── rdbinstance_editor_role.yaml │ ├── rdbinstance_viewer_role.yaml │ ├── rdbuser_editor_role.yaml │ ├── rdbuser_viewer_role.yaml │ ├── role.yaml │ └── role_binding.yaml ├── samples │ ├── rdb_v1alpha1_rdbdatabase.yaml │ ├── rdb_v1alpha1_rdbinstance.yaml │ └── rdb_v1alpha1_rdbuser.yaml └── webhook │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ ├── manifests.yaml │ └── service.yaml ├── controllers ├── rdb │ ├── rdbdatabase_controller.go │ ├── rdbinstance_controller.go │ └── rdbuser_controller.go ├── scaleway_controller.go └── scaleway_controller_test.go ├── deploy └── scaleway-operator-secrets.yml ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── internal └── testhelpers │ └── httprecorder │ └── recorder.go ├── main.go ├── pkg ├── manager │ ├── rdb │ │ ├── database.go │ │ ├── database_webhook.go │ │ ├── instance.go │ │ ├── instance_webhook.go │ │ └── user.go │ └── scaleway │ │ └── manager.go └── utils │ └── labels.go └── webhooks ├── rdb ├── rdbdatabase_webhook.go └── rdbinstance_webhook.go ├── scaleway_webhook.go ├── utils.go └── utils_test.go /.github/workflows/merge.yaml: -------------------------------------------------------------------------------- 1 | name: merge 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | jobs: 7 | merge_main: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | - name: Unshallow 13 | run: git fetch --prune --unshallow 14 | - name: Extract branch name 15 | shell: bash 16 | run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" 17 | id: extract_branch 18 | - name: Enable experimental on dockerd 19 | run: | 20 | echo $'{\n "experimental": true\n}' | sudo tee /etc/docker/daemon.json 21 | sudo service docker restart 22 | - name: Set up Docker Buildx 23 | id: buildx 24 | uses: crazy-max/ghaction-docker-buildx@v1 25 | with: 26 | version: latest 27 | - name: Docker login 28 | run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USER }} --password-stdin 29 | - name: Make latest release 30 | run: make release 31 | env: 32 | DOCKER_CLI_EXPERIMENTAL: enabled 33 | IMAGE_TAG: latest 34 | - name: Make tagged release 35 | run: make release 36 | env: 37 | DOCKER_CLI_EXPERIMENTAL: enabled 38 | IMAGE_TAG: ${{ github.sha }} 39 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: pull-request 2 | 3 | on: 4 | - pull_request 5 | 6 | jobs: 7 | unit-test: 8 | strategy: 9 | matrix: 10 | go-version: [1.15.x] 11 | platform: [ubuntu-latest] 12 | runs-on: ${{ matrix.platform }} 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@v1 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | - name: checkout 19 | uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 1 22 | - name: Run unit tests 23 | run: make test 24 | build-test: 25 | strategy: 26 | matrix: 27 | go-version: [1.15.x] 28 | platform: [ubuntu-latest] 29 | arch: [amd64, arm, arm64] 30 | runs-on: ${{ matrix.platform }} 31 | steps: 32 | - name: Install Go 33 | uses: actions/setup-go@v1 34 | with: 35 | go-version: ${{ matrix.go-version }} 36 | - name: checkout 37 | uses: actions/checkout@v2 38 | with: 39 | fetch-depth: 1 40 | - name: Building binary 41 | run: GOARCH=${{ matrix.arch }} make compile 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | - name: Unshallow 13 | run: git fetch --prune --unshallow 14 | - name: Highest tag 15 | id: highestTag 16 | run: | 17 | git fetch --depth=1 origin +refs/tags/*:refs/tags/* 18 | echo ::set-output name=highest::$(git tag | grep -E "^v?([0-9]+\.)+[0-9]+$" | sort -r -V | head -n1) 19 | - name: Enable experimental on dockerd 20 | run: | 21 | echo $'{\n "experimental": true\n}' | sudo tee /etc/docker/daemon.json 22 | sudo service docker restart 23 | - name: Set up Docker Buildx 24 | id: buildx 25 | uses: crazy-max/ghaction-docker-buildx@v1 26 | with: 27 | version: latest 28 | - name: Docker login 29 | run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USER }} --password-stdin 30 | - name: Get tag 31 | uses: olegtarasov/get-tag@v1 32 | id: tagName 33 | - name: Make release 34 | run: make release 35 | env: 36 | DOCKER_CLI_EXPERIMENTAL: enabled 37 | IMAGE_TAG: ${{ steps.tagName.outputs.tag }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | main 10 | scaleway-operator 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Kubernetes Generated files - skip generated files, except for vendored files 19 | 20 | !vendor/**/zz_generated.* 21 | 22 | # editor and IDE paraphernalia 23 | .idea 24 | *.swp 25 | *.swo 26 | *~ 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to `scaleway-operator` 2 | 3 | `scaleway-operator` is Apache 2.0 licensed and accepts contributions via GitHub. 4 | This document will cover how to contribute to the project and report issues. 5 | 6 | 7 | ## Topics 8 | 9 | - [Reporting Security Issues](#reporting-security-issues) 10 | - [Reporting Issues](#reporting-issues) 11 | - [Suggesting feature](#suggesting-feature) 12 | - [Contributing Code](#contributing-code) 13 | - [Community Guidelines](#community-guidelines) 14 | 15 | ## Reporting security issues 16 | 17 | At Scaleway we take security seriously. 18 | If you have any issue regarding security, please notify us by sending an email to [security@scaleway.com](mailto:security@scaleway.com). 19 | 20 | Please _DO NOT_ create a GitHub issue. 21 | 22 | We will follow up with you promptly with more information and a plan for remediation. 23 | We currently do not offer a paid security bounty program, but we would love to send some Scaleway swag your way along with our deepest gratitude for your assistance in making Scaleway a more secure Cloud ecosystem. 24 | 25 | ## Reporting issues 26 | 27 | A great way to contribute to the project is to send a detailed report when you encounter a bug. 28 | We always appreciate a well-written, thorough bug report, and will thank you for it! 29 | Before opening a new issue, we appreciate you reviewing open issues to see if there are any similar requests. 30 | If there is a match, thumbs up the issue with a 👍 and leave a comment if you have additional information. 31 | 32 | When reporting an issue, include the following: 33 | 34 | - The version of `scaleway-operator` you are using (v0.1.3, v0.2.4, main, ...) 35 | - Kubernetes apiserver version, flags and environments used. 36 | - Kubelet version, flags and environments used. 37 | - Go version 38 | - GOOS 39 | - GOARCH 40 | 41 | ## Suggesting a feature 42 | 43 | When requesting a feature, some of the questions we want to answer are: 44 | 45 | - What value does this feature bring to end users ? 46 | - How urgent is the need (nice to have feature or need to have) ? 47 | - Does this align with the goals of `scaleway-operator` ? 48 | 49 | ### Submit code 50 | 51 | To submit code: 52 | 53 | - Create a fork of the project 54 | - Create a topic branch from where you want to base your work (usually main) 55 | - Add tests to cover contributed code 56 | - Push your commit(s) to your topic branch on your fork 57 | - Open a pull request against `scaleway-operator` main branch that follows [PR guidelines](#pull-request-guidelines) 58 | 59 | The maintainers of `scaleway-operator` use a "Let's Get This Merged" (LGTM) message in the pull request to note that the commits are ready to merge. 60 | After one or more maintainer states LGTM, we will merge. 61 | If you have questions or comments on your code, feel free to correct these in your branch through new commits. 62 | 63 | ### Pull Request Guidelines 64 | 65 | The goal of the following guidelines is to have Pull Requests (PRs) that are fairly easy to review and comprehend, and code that is easy to maintain in the future. 66 | 67 | - **Pull Request title should be clear** on what is being fixed or added to the code base. 68 | If you are addressing an open issue, please start the title with "fix: #XXX" or "feature: #XXX" 69 | - **Keep it readable for human reviewers** and prefer a subset of functionality (code) with tests and documentation over delivering them separately 70 | - **Don't forget commenting code** to help reviewers understand your code 71 | - **Notify Work In Progress PRs** by prefixing the title with `[WIP]` 72 | - **Please, keep us updated.** 73 | We will try our best to merge your PR, but please notice that PRs may be closed after 30 days of inactivity. 74 | 75 | Your pull request should be rebased against the current main branch. Please do not merge 76 | the current main branch in with your topic branch, nor use the Update Branch button provided 77 | by GitHub on the pull request page. 78 | 79 | Keep in mind only the **Pull Request Title** will be used as commit message as we stash all commits on merge. 80 | 81 | ## Community guidelines 82 | 83 | Thank you for reading through all of this, if you have any question feel free to [reach us](README.md#reach-us)! 84 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.15.2-alpine as builder 2 | 3 | RUN apk update && apk add --no-cache git ca-certificates && update-ca-certificates 4 | 5 | WORKDIR /go/src/github.com/scaleway/scaleway-operator 6 | 7 | COPY go.mod go.mod 8 | COPY go.sum go.sum 9 | RUN go mod download 10 | 11 | COPY main.go main.go 12 | COPY apis/ apis/ 13 | COPY pkg/ pkg/ 14 | COPY webhooks/ webhooks/ 15 | COPY controllers/ controllers/ 16 | COPY internal/ internal/ 17 | 18 | RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -mod=readonly -a -o scaleway-operator main.go 19 | 20 | FROM scratch 21 | WORKDIR / 22 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 23 | COPY --from=builder /go/src/github.com/scaleway/scaleway-operator/scaleway-operator . 24 | ENTRYPOINT ["/scaleway-operator"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | https://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright 2019 Scaleway. 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | https://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OS ?= $(shell go env GOOS) 2 | ARCH ?= $(shell go env GOARCH) 3 | ALL_PLATFORM = linux/amd64,linux/arm/v7,linux/arm64 4 | 5 | # Image URL to use all building/pushing image targets 6 | REGISTRY ?= scaleway 7 | IMAGE ?= scaleway-operator 8 | FULL_IMAGE ?= $(REGISTRY)/$(IMAGE) 9 | 10 | IMAGE_TAG ?= $(shell git rev-parse HEAD) 11 | 12 | DOCKER_CLI_EXPERIMENTAL ?= enabled 13 | 14 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 15 | CRD_OPTIONS ?= "crd:crdVersions=v1" 16 | 17 | ifdef TMPDIR 18 | TMPDIR := $(realpath ${TMPDIR}) 19 | else 20 | TMPDIR := /tmp 21 | endif 22 | 23 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 24 | ifeq (,$(shell go env GOBIN)) 25 | GOBIN=$(shell go env GOPATH)/bin 26 | else 27 | GOBIN=$(shell go env GOBIN) 28 | endif 29 | 30 | all: compile 31 | 32 | generate-test-certs: CONFIGTXT := $(shell mktemp) 33 | generate-test-certs: WEBHOOK_DIR := $(TMPDIR)/k8s-webhook-server 34 | generate-test-certs: WEBHOOK_CERT_DIR := $(TMPDIR)/k8s-webhook-server/serving-certs 35 | generate-test-certs: 36 | rm -rf $(WEBHOOK_DIR) 37 | mkdir -p $(WEBHOOK_CERT_DIR) 38 | 39 | @echo "[req]" > $(CONFIGTXT) 40 | @echo "distinguished_name = req_distinguished_name" >> $(CONFIGTXT) 41 | @echo "x509_extensions = v3_req" >> $(CONFIGTXT) 42 | @echo "req_extensions = req_ext" >> $(CONFIGTXT) 43 | @echo "[req_distinguished_name]" >> $(CONFIGTXT) 44 | @echo "[req_ext]" >> $(CONFIGTXT) 45 | @echo "subjectAltName = @alt_names" >> $(CONFIGTXT) 46 | @echo "[v3_req]" >> $(CONFIGTXT) 47 | @echo "subjectAltName = @alt_names" >> $(CONFIGTXT) 48 | @echo "[alt_names]" >> $(CONFIGTXT) 49 | @echo "DNS.1 = scaleway-operator-webhook-service.scaleway-operator-system.svc.cluster.local" >> $(CONFIGTXT) 50 | @echo "IP.1 = 127.0.0.1" >> $(CONFIGTXT) 51 | 52 | @echo "OpenSSL Config:" 53 | @cat $(CONFIGTXT) 54 | @echo 55 | 56 | openssl req -x509 -days 730 -out $(WEBHOOK_CERT_DIR)/tls.crt -keyout $(WEBHOOK_CERT_DIR)/tls.key -newkey rsa:4096 -subj "/CN=scaleway-operator-webhook-service.scaleway-operator-system" -config $(CONFIGTXT) -nodes 57 | 58 | # Run tests 59 | test: generate fmt vet manifests 60 | go test ./... -coverprofile cover.out 61 | git diff --exit-code 62 | 63 | # Run against the configured Kubernetes cluster in ~/.kube/config 64 | run: generate fmt vet manifests 65 | go run ./main.go 66 | 67 | # Install CRDs into a cluster 68 | install: manifests 69 | kustomize build config/crd | kubectl apply -f - 70 | 71 | # Uninstall CRDs from a cluster 72 | uninstall: manifests 73 | kustomize build config/crd | kubectl delete -f - 74 | 75 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 76 | deploy: manifests 77 | cd config/manager && kustomize edit set image controller=${FULL_IMAGE} 78 | kustomize build config/default | kubectl apply -f - 79 | 80 | # Generate manifests e.g. CRD, RBAC etc. 81 | manifests: controller-gen 82 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases 83 | 84 | # Run go fmt against code 85 | fmt: 86 | go fmt ./... 87 | 88 | # Run go vet against code 89 | vet: 90 | go vet ./... 91 | 92 | # Generate code 93 | generate: controller-gen 94 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 95 | 96 | compile: 97 | go build -v -o scaleway-operator main.go 98 | 99 | docker-build: 100 | @echo "Building scaleway-operator for ${ARCH}" 101 | docker build . --platform=linux/$(ARCH) -f Dockerfile -t ${FULL_IMAGE}:${IMAGE_TAG}-$(ARCH) 102 | 103 | docker-buildx-all: 104 | @echo "Making release for tag $(IMAGE_TAG)" 105 | docker buildx build --platform=$(ALL_PLATFORM) --push -t $(FULL_IMAGE):$(IMAGE_TAG) . 106 | 107 | release: docker-buildx-all 108 | 109 | # find or download controller-gen 110 | # download controller-gen if necessary 111 | controller-gen: 112 | ifeq (, $(shell which controller-gen)) 113 | @{ \ 114 | set -e ;\ 115 | CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\ 116 | cd $$CONTROLLER_GEN_TMP_DIR ;\ 117 | go mod init tmp ;\ 118 | go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.3.0 ;\ 119 | rm -rf $$CONTROLLER_GEN_TMP_DIR ;\ 120 | } 121 | CONTROLLER_GEN=$(GOBIN)/controller-gen 122 | else 123 | CONTROLLER_GEN=$(shell which controller-gen) 124 | endif 125 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | reviewers: 2 | - jtherin 3 | - Sh4d1 4 | - rleone 5 | approvers: 6 | - jtherin 7 | - Sh4d1 8 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: scaleway.com 2 | multigroup: true 3 | repo: github.com/scaleway/scaleway-operator 4 | resources: 5 | - group: rdb 6 | kind: RDBInstance 7 | version: v1alpha1 8 | - group: rdb 9 | kind: RDBDatabase 10 | version: v1alpha1 11 | - group: rdb 12 | kind: RDBUser 13 | version: v1alpha1 14 | version: "2" 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/scaleway/scaleway-operator)](https://goreportcard.com/report/github.com/scaleway/scaleway-operator) 2 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/scaleway/scaleway-operator) 3 | ![GitHub](https://img.shields.io/github/license/scaleway/scaleway-operator?style=flat) 4 | 5 | # Scaleway Operator for Kubernetes 6 | 7 | ⚠️ this project is not maintained anymore. 8 | 9 | The **Scaleway Operator** is a Kubernetes controller that lets you create Scaleway Resources directly from Kubernetes via [Kubernetes Custom Resources Definitions](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/). 10 | 11 | **WARNING**: this project is under active development and should be considered alpha. 12 | 13 | ## Features 14 | 15 | Currently, **Scaleway Operator** only supports RDB instances, databases and users. Other resources will be implemented, and [contributions](./CONTRIBUTING.md) are more than welcome! 16 | 17 | If you want to see a specific Scaleway product, please [open an issue](https://github.com/scaleway/scaleway-operator/issues/new) describing which product you'd like to see. 18 | 19 | ## Getting Started 20 | 21 | First step is to install [Cert Manager](https://cert-manager.io/docs/installation/kubernetes/) in order to handle the webhooks certificates. 22 | 23 | ```bash 24 | $ kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v1.0.3/cert-manager.yaml 25 | ``` 26 | 27 | Once Cert Manager is up and running, you have to install the CRDs. First clone the repo, and then making sure your `KUBECONFIG` environment variable is set on the right cluster, run: 28 | ```bash 29 | $ make install 30 | ``` 31 | 32 | Then, run: 33 | ```bash 34 | kubectl create -f deploy/scaleway-operator-secrets.yml --edit --namespace=scaleway-operator-system 35 | ``` 36 | 37 | and replace the values. 38 | 39 | Finally, in order to deploy the Scaleway Operator, run: 40 | ```bash 41 | kubectl apply -k config/default 42 | ``` 43 | 44 | ## Development 45 | 46 | If you are looking for a way to contribute please read [CONTRIBUTING](./CONTRIBUTING.md). 47 | 48 | ### Code of conduct 49 | 50 | Participation in the Kubernetes community is governed by the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). 51 | 52 | ## Reach us 53 | 54 | We love feedback. Feel free to reach us on [Scaleway Slack community](https://slack.scaleway.com), we are waiting for you on #k8s. 55 | 56 | You can also join the official Kubernetes slack on #scaleway-k8s channel 57 | 58 | You can also [raise an issue](https://github.com/scaleway/scaleway-operator/issues/new) if you think you've found a bug. 59 | -------------------------------------------------------------------------------- /apis/meta/v1alpha1/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 v1alpha1 contains API Schema definitions for the meta v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=meta.scaleway.com 20 | package v1alpha1 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: "meta.scaleway.com", Version: "v1alpha1"} 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/meta/v1alpha1/meta.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // TypeMeta implements the Scaleway Status methods 4 | // +k8s:deepcopy-gen=false 5 | type TypeMeta interface { 6 | GetStatus() Status 7 | SetStatus(Status) 8 | } 9 | -------------------------------------------------------------------------------- /apis/meta/v1alpha1/status_helpers.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "time" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | ) 8 | 9 | // IsReady returns true is the Ready condition is True 10 | func (s Status) IsReady() bool { 11 | for _, c := range s.Conditions { 12 | if c.Type == Ready && c.Status == corev1.ConditionTrue { 13 | return true 14 | } 15 | } 16 | return false 17 | } 18 | 19 | // IsReconciled returns true is the Ready condition is True 20 | func (s Status) IsReconciled() bool { 21 | for _, c := range s.Conditions { 22 | if c.Type == Reconciled && c.Status == corev1.ConditionTrue { 23 | return true 24 | } 25 | } 26 | return false 27 | } 28 | 29 | // IsReconciledAfter returns true is the Ready condition is True 30 | func (s Status) IsReconciledAfter(now time.Time) bool { 31 | for _, c := range s.Conditions { 32 | if c.Type == Reconciled && c.Status == corev1.ConditionTrue && c.LastTransitionTime.After(now) { 33 | return true 34 | } 35 | } 36 | return false 37 | } 38 | -------------------------------------------------------------------------------- /apis/meta/v1alpha1/status_types.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | ) 7 | 8 | // Status defines the observed state of a Scaleway resource 9 | type Status struct { 10 | Conditions []Condition `json:"conditions,omitempty"` 11 | } 12 | 13 | // Condition contains details for the current condition of this Scaleway resource. 14 | type Condition struct { 15 | // Type is the type of the condition. 16 | Type ConditionType `json:"type,omitempty"` 17 | // Status is the status of the condition. 18 | // Can be True, False, Unknown. 19 | Status corev1.ConditionStatus `json:"status,omitempty"` 20 | // Last time we probed the condition. 21 | LastProbeTime metav1.Time `json:"lastProbeTime,omitempty"` 22 | // Last time the condition transitioned from one status to another. 23 | LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` 24 | // Unique, one-word, CamelCase reason for the condition's last transition. 25 | Reason string `json:"reason,omitempty"` 26 | // Human-readable message indicating details about last transition. 27 | Message string `json:"message,omitempty"` 28 | } 29 | 30 | // ConditionType is a valid value for Conidtion.Type 31 | type ConditionType string 32 | 33 | const ( 34 | // Reconciled indicates whether the resource was successfully reconciled 35 | Reconciled ConditionType = "Reconciled" 36 | // Ready indicates whether the resource is considered ready 37 | Ready ConditionType = "Ready" 38 | ) 39 | 40 | //func init() { 41 | // SchemeBuilder.Register(&Status{}) 42 | //} 43 | -------------------------------------------------------------------------------- /apis/meta/v1alpha1/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 v1alpha1 22 | 23 | import () 24 | 25 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 26 | func (in *Condition) DeepCopyInto(out *Condition) { 27 | *out = *in 28 | in.LastProbeTime.DeepCopyInto(&out.LastProbeTime) 29 | in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) 30 | } 31 | 32 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. 33 | func (in *Condition) DeepCopy() *Condition { 34 | if in == nil { 35 | return nil 36 | } 37 | out := new(Condition) 38 | in.DeepCopyInto(out) 39 | return out 40 | } 41 | 42 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 43 | func (in *Status) DeepCopyInto(out *Status) { 44 | *out = *in 45 | if in.Conditions != nil { 46 | in, out := &in.Conditions, &out.Conditions 47 | *out = make([]Condition, len(*in)) 48 | for i := range *in { 49 | (*in)[i].DeepCopyInto(&(*out)[i]) 50 | } 51 | } 52 | } 53 | 54 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status. 55 | func (in *Status) DeepCopy() *Status { 56 | if in == nil { 57 | return nil 58 | } 59 | out := new(Status) 60 | in.DeepCopyInto(out) 61 | return out 62 | } 63 | -------------------------------------------------------------------------------- /apis/rdb/v1alpha1/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 v1alpha1 contains API Schema definitions for the rdb v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=rdb.scaleway.com 20 | package v1alpha1 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: "rdb.scaleway.com", Version: "v1alpha1"} 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/rdb/v1alpha1/rdbdatabase_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 v1alpha1 18 | 19 | import ( 20 | scalewaymetav1alpha1 "github.com/scaleway/scaleway-operator/apis/meta/v1alpha1" 21 | "k8s.io/apimachinery/pkg/api/resource" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | ) 24 | 25 | // RDBDatabaseSpec defines the desired state of RDBDatabase 26 | type RDBDatabaseSpec struct { 27 | // InstanceRef represents the reference to the instance of the database 28 | InstanceRef RDBInstanceRef `json:"instanceRef"` 29 | // OverrideName represents the name given to the database 30 | // This field is immutable after creation 31 | // +optional 32 | OverrideName string `json:"overrideName,omitempty"` 33 | } 34 | 35 | // RDBInstanceRef defines a reference to rdb instance 36 | // Only one of ExternalID/Region or Name/Namespace must be specified 37 | type RDBInstanceRef struct { 38 | // ExternalID is the ID of the instance 39 | // This field is immutable after creation 40 | // +optional 41 | ExternalID string `json:"externalID,omitempty"` 42 | // Region is the region of the instance 43 | // This field is immutable after creation 44 | // +optional 45 | Region string `json:"region,omitempty"` 46 | // Name is the name of the instance of this database 47 | // This field is immutable after creation 48 | // +optional 49 | Name string `json:"name,omitempty"` 50 | // Namespace is the namespace of the instance of this database 51 | // If empty, it will use the namespace of the database 52 | // This field is immutable after creation 53 | // +optional 54 | Namespace string `json:"namespace,omitempty"` 55 | } 56 | 57 | // RDBDatabaseStatus defines the observed state of RDBDatabase 58 | type RDBDatabaseStatus struct { 59 | // Size represents the size of the database 60 | Size *resource.Quantity `json:"size,omitempty"` 61 | // Managed defines whether this database is mananged 62 | Managed bool `json:"managed,omitempty"` 63 | // Owner represents the owner of this database 64 | Owner string `json:"owner,omitempty"` 65 | // Conditions is the current conditions of the RDBDatabase 66 | scalewaymetav1alpha1.Status `json:",inline"` 67 | } 68 | 69 | // +kubebuilder:object:root=true 70 | // +kubebuilder:subresource:status 71 | // +kubebuilder:resource:shortName=rdbd;rdbdatabase 72 | // +kubebuilder:printcolumn:name="size",type="string",JSONPath=".status.size" 73 | // +kubebuilder:printcolumn:name="owner",type="string",JSONPath=".status.owner" 74 | // +kubebuilder:printcolumn:name="managed",type="string",JSONPath=".status.managed" 75 | 76 | // RDBDatabase is the Schema for the rdbdatabases API 77 | type RDBDatabase struct { 78 | metav1.TypeMeta `json:",inline"` 79 | metav1.ObjectMeta `json:"metadata,omitempty"` 80 | 81 | Spec RDBDatabaseSpec `json:"spec,omitempty"` 82 | Status RDBDatabaseStatus `json:"status,omitempty"` 83 | } 84 | 85 | // GetStatus returns the scaleway meta status 86 | func (r *RDBDatabase) GetStatus() scalewaymetav1alpha1.Status { 87 | return r.Status.Status 88 | } 89 | 90 | // SetStatus sets the scaleway meta status 91 | func (r *RDBDatabase) SetStatus(status scalewaymetav1alpha1.Status) { 92 | r.Status.Status = status 93 | } 94 | 95 | // +kubebuilder:object:root=true 96 | 97 | // RDBDatabaseList contains a list of RDBDatabase 98 | type RDBDatabaseList struct { 99 | metav1.TypeMeta `json:",inline"` 100 | metav1.ListMeta `json:"metadata,omitempty"` 101 | Items []RDBDatabase `json:"items"` 102 | } 103 | 104 | func init() { 105 | SchemeBuilder.Register(&RDBDatabase{}, &RDBDatabaseList{}) 106 | } 107 | -------------------------------------------------------------------------------- /apis/rdb/v1alpha1/rdbinstance_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 v1alpha1 18 | 19 | import ( 20 | scalewaymetav1alpha1 "github.com/scaleway/scaleway-operator/apis/meta/v1alpha1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // RDBInstanceSpec defines the desired state of RDBInstance 25 | type RDBInstanceSpec struct { 26 | // InstanceID is the ID of the instance 27 | // If empty it will create a new instance 28 | // If set it will use this ID as the instance ID 29 | // This field is immutable after creation 30 | // At most one of InstanceID/Region and InstanceFrom have to be specified 31 | // on creation. 32 | // +optional 33 | InstanceID string `json:"instanceID,omitempty"` 34 | // Region is the region in which the RDBInstance will run 35 | // This field is immutable after creation 36 | // Defaults to the controller default region 37 | // At most one of InstanceID/Region and InstanceFrom have to be specified 38 | // on creation. 39 | // +optional 40 | Region string `json:"region,omitempty"` 41 | // InstanceFrom allows to create an instance from an existing one 42 | // At most one of InstanceID/Region and InstanceFrom have to be specified 43 | // on creation. 44 | // This field is immutable after creation 45 | // +optional 46 | InstanceFrom *RDBInstanceRef `json:"instanceFrom,omitempty"` 47 | // Engine is the database engine of the RDBInstance 48 | Engine string `json:"engine"` 49 | // NodeType is the type of node to use for the RDBInstance 50 | NodeType string `json:"nodeType"` 51 | // IsHaCluster represents whether the RDBInstance should be in HA mode 52 | // Defaults to false 53 | // +kubebuilder:default:false 54 | // +optional 55 | IsHaCluster bool `json:"isHaCluster,omitempty"` 56 | // AutoBackup represents the RDBInstance auto backup policy 57 | // +optional 58 | AutoBackup *RDBInstanceAutoBackup `json:"autoBackup,omitempty"` 59 | // ACL represents the ACL rules of the RDBInstance 60 | ACL *RDBACL `json:"acl,omitempty"` 61 | } 62 | 63 | // RDBACL defines the acl of a RDBInstance 64 | type RDBACL struct { 65 | // Rules represents the RDB ACL rules 66 | // +optional 67 | Rules []RDBACLRule `json:"rules,omitempty"` 68 | 69 | // AllowCluster represents wether the nodes in the cluster 70 | // should be allowed 71 | // +optional 72 | AllowCluster bool `json:"allowCluster,omitempty"` 73 | } 74 | 75 | // RDBACLRule defines a rule for a RDB ACL 76 | type RDBACLRule struct { 77 | // IPRange represents a CIDR IP range 78 | IPRange string `json:"ipRange"` 79 | // Description is the description associated with this ACL rule 80 | // +optional 81 | Description string `json:"description,omitempty"` 82 | } 83 | 84 | // RDBInstanceAutoBackup defines the auto backup state of a RDBInstance 85 | type RDBInstanceAutoBackup struct { 86 | // Disabled represents whether the auto backup should be disabled 87 | Disabled bool `json:"disabled,omitempty"` 88 | // Frequency represents the frequency, in hour, at which auto backups are made 89 | // Default to 24 90 | // +kubebuilder:default:24 91 | // +kubebuilder:validation:Minimum=0 92 | // +optional 93 | Frequency *int32 `json:"frequency,omitempty"` 94 | // Retention represents the number of days the autobackup are kept 95 | // Default to 7 96 | // +kubebuilder:default:7 97 | // +kubebuilder:validation:Minimum=0 98 | // +optional 99 | Retention *int32 `json:"retention,omitempty"` 100 | } 101 | 102 | // RDBInstanceStatus defines the observed state of RDBInstance 103 | type RDBInstanceStatus struct { 104 | // Endpoint is the endpoint of the RDBInstance 105 | Endpoint RDBInstanceEndpoint `json:"endpoint,omitempty"` 106 | // Conditions is the current conditions of the RDBInstance 107 | scalewaymetav1alpha1.Status `json:",inline"` 108 | } 109 | 110 | // RDBInstanceEndpoint defines the endpoint of a RDBInstance 111 | type RDBInstanceEndpoint struct { 112 | // IP is the IP of the RDBInstance 113 | IP string `json:"ip,omitempty"` 114 | // Port if the port of the RDBInstance 115 | Port int32 `json:"port,omitempty"` 116 | } 117 | 118 | // +kubebuilder:object:root=true 119 | // +kubebuilder:subresource:status 120 | // +kubebuilder:resource:shortName=rdbi;rdbinstance 121 | // +kubebuilder:printcolumn:name="IP",type="string",JSONPath=".status.endpoint.ip" 122 | // +kubebuilder:printcolumn:name="Port",type="integer",JSONPath=".status.endpoint.port" 123 | 124 | // RDBInstance is the Schema for the databaseinstances API 125 | type RDBInstance struct { 126 | metav1.TypeMeta `json:",inline"` 127 | metav1.ObjectMeta `json:"metadata,omitempty"` 128 | 129 | Spec RDBInstanceSpec `json:"spec,omitempty"` 130 | Status RDBInstanceStatus `json:"status,omitempty"` 131 | } 132 | 133 | // GetStatus returns the scaleway meta status 134 | func (r *RDBInstance) GetStatus() scalewaymetav1alpha1.Status { 135 | return r.Status.Status 136 | } 137 | 138 | // SetStatus sets the scaleway meta status 139 | func (r *RDBInstance) SetStatus(status scalewaymetav1alpha1.Status) { 140 | r.Status.Status = status 141 | } 142 | 143 | // +kubebuilder:object:root=true 144 | 145 | // RDBInstanceList contains a list of RDBInstance 146 | type RDBInstanceList struct { 147 | metav1.TypeMeta `json:",inline"` 148 | metav1.ListMeta `json:"metadata,omitempty"` 149 | Items []RDBInstance `json:"items"` 150 | } 151 | 152 | func init() { 153 | SchemeBuilder.Register(&RDBInstance{}, &RDBInstanceList{}) 154 | } 155 | -------------------------------------------------------------------------------- /apis/rdb/v1alpha1/rdbuser_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 v1alpha1 18 | 19 | import ( 20 | scalewaymetav1alpha1 "github.com/scaleway/scaleway-operator/apis/meta/v1alpha1" 21 | corev1 "k8s.io/api/core/v1" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | ) 24 | 25 | // RDBUserSpec defines the desired state of RDBUser 26 | type RDBUserSpec struct { 27 | // UserName is the user name to be created on the RDBInstance 28 | UserName string `json:"userName"` 29 | // Password is the password associated to the user 30 | Password RDBInstancePassword `json:"password"` 31 | // Admin represents whether the user is an admin user 32 | // +kubebuilder:default:true 33 | // +optional 34 | Admin bool `json:"admin,omitempty"` 35 | // Privileges represents the privileges given to this user 36 | Privileges []RDBPrivilege `json:"privileges,omitempty"` 37 | // InstanceRef represents the reference to the instance of the user 38 | InstanceRef RDBInstanceRef `json:"instanceRef"` 39 | } 40 | 41 | // RDBPrivilege defines a privilege linked to a RDBUser 42 | type RDBPrivilege struct { 43 | // DatabaseName is the name to a RDB Database for this privilege 44 | DatabaseName string `json:"databaseRef"` 45 | // Permission is the given permission for this privilege 46 | Permission RDBPermission `json:"permission"` 47 | } 48 | 49 | // RDBPermission defines a permission for a privilege 50 | // +kubebuilder:validation:Enum=ReadOnly;ReadWrite;All;None 51 | type RDBPermission string 52 | 53 | const ( 54 | // PermissionReadOnly is the readonly permission 55 | PermissionReadOnly RDBPermission = "ReadOnly" 56 | // PermissionReadWrite is the readwrite permission 57 | PermissionReadWrite RDBPermission = "ReadWrite" 58 | // PermissionAll is the all permission 59 | PermissionAll RDBPermission = "All" 60 | // PermissionNone is the none permission 61 | PermissionNone RDBPermission = "None" 62 | ) 63 | 64 | // RDBInstancePassword defines the password of a RDBInstance 65 | // One of Value or ValueFrom must be specified 66 | type RDBInstancePassword struct { 67 | // Value represents a raw value 68 | // +optional 69 | Value *string `json:"value,omitempty"` 70 | // ValueFrom represents a value from a secret 71 | // +optional 72 | ValueFrom *RDBInstancePasswordValueFrom `json:"valueFrom,omitempty"` 73 | } 74 | 75 | // RDBInstancePasswordValueFrom defines a source to get a password from 76 | type RDBInstancePasswordValueFrom struct { 77 | SecretKeyRef corev1.SecretReference `json:"secretKeyRef"` 78 | } 79 | 80 | // RDBUserStatus defines the observed state of RDBUser 81 | type RDBUserStatus struct { 82 | // Conditions is the current conditions of the RDBInstance 83 | scalewaymetav1alpha1.Status `json:",inline"` 84 | } 85 | 86 | // +kubebuilder:object:root=true 87 | // +kubebuilder:subresource:status 88 | // +kubebuilder:resource:shortName=rdbu;rdbuser 89 | // +kubebuilder:printcolumn:name="UserName",type="string",JSONPath=".spec.userName" 90 | 91 | // RDBUser is the Schema for the rdbusers API 92 | type RDBUser struct { 93 | metav1.TypeMeta `json:",inline"` 94 | metav1.ObjectMeta `json:"metadata,omitempty"` 95 | 96 | Spec RDBUserSpec `json:"spec,omitempty"` 97 | Status RDBUserStatus `json:"status,omitempty"` 98 | } 99 | 100 | // +kubebuilder:object:root=true 101 | 102 | // RDBUserList contains a list of RDBUser 103 | type RDBUserList struct { 104 | metav1.TypeMeta `json:",inline"` 105 | metav1.ListMeta `json:"metadata,omitempty"` 106 | Items []RDBUser `json:"items"` 107 | } 108 | 109 | func init() { 110 | SchemeBuilder.Register(&RDBUser{}, &RDBUserList{}) 111 | } 112 | -------------------------------------------------------------------------------- /apis/rdb/v1alpha1/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 v1alpha1 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 *RDBACL) DeepCopyInto(out *RDBACL) { 29 | *out = *in 30 | if in.Rules != nil { 31 | in, out := &in.Rules, &out.Rules 32 | *out = make([]RDBACLRule, len(*in)) 33 | copy(*out, *in) 34 | } 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBACL. 38 | func (in *RDBACL) DeepCopy() *RDBACL { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(RDBACL) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 48 | func (in *RDBACLRule) DeepCopyInto(out *RDBACLRule) { 49 | *out = *in 50 | } 51 | 52 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBACLRule. 53 | func (in *RDBACLRule) DeepCopy() *RDBACLRule { 54 | if in == nil { 55 | return nil 56 | } 57 | out := new(RDBACLRule) 58 | in.DeepCopyInto(out) 59 | return out 60 | } 61 | 62 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 63 | func (in *RDBDatabase) DeepCopyInto(out *RDBDatabase) { 64 | *out = *in 65 | out.TypeMeta = in.TypeMeta 66 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 67 | out.Spec = in.Spec 68 | in.Status.DeepCopyInto(&out.Status) 69 | } 70 | 71 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBDatabase. 72 | func (in *RDBDatabase) DeepCopy() *RDBDatabase { 73 | if in == nil { 74 | return nil 75 | } 76 | out := new(RDBDatabase) 77 | in.DeepCopyInto(out) 78 | return out 79 | } 80 | 81 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 82 | func (in *RDBDatabase) DeepCopyObject() runtime.Object { 83 | if c := in.DeepCopy(); c != nil { 84 | return c 85 | } 86 | return nil 87 | } 88 | 89 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 90 | func (in *RDBDatabaseList) DeepCopyInto(out *RDBDatabaseList) { 91 | *out = *in 92 | out.TypeMeta = in.TypeMeta 93 | in.ListMeta.DeepCopyInto(&out.ListMeta) 94 | if in.Items != nil { 95 | in, out := &in.Items, &out.Items 96 | *out = make([]RDBDatabase, len(*in)) 97 | for i := range *in { 98 | (*in)[i].DeepCopyInto(&(*out)[i]) 99 | } 100 | } 101 | } 102 | 103 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBDatabaseList. 104 | func (in *RDBDatabaseList) DeepCopy() *RDBDatabaseList { 105 | if in == nil { 106 | return nil 107 | } 108 | out := new(RDBDatabaseList) 109 | in.DeepCopyInto(out) 110 | return out 111 | } 112 | 113 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 114 | func (in *RDBDatabaseList) DeepCopyObject() runtime.Object { 115 | if c := in.DeepCopy(); c != nil { 116 | return c 117 | } 118 | return nil 119 | } 120 | 121 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 122 | func (in *RDBDatabaseSpec) DeepCopyInto(out *RDBDatabaseSpec) { 123 | *out = *in 124 | out.InstanceRef = in.InstanceRef 125 | } 126 | 127 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBDatabaseSpec. 128 | func (in *RDBDatabaseSpec) DeepCopy() *RDBDatabaseSpec { 129 | if in == nil { 130 | return nil 131 | } 132 | out := new(RDBDatabaseSpec) 133 | in.DeepCopyInto(out) 134 | return out 135 | } 136 | 137 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 138 | func (in *RDBDatabaseStatus) DeepCopyInto(out *RDBDatabaseStatus) { 139 | *out = *in 140 | if in.Size != nil { 141 | in, out := &in.Size, &out.Size 142 | x := (*in).DeepCopy() 143 | *out = &x 144 | } 145 | in.Status.DeepCopyInto(&out.Status) 146 | } 147 | 148 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBDatabaseStatus. 149 | func (in *RDBDatabaseStatus) DeepCopy() *RDBDatabaseStatus { 150 | if in == nil { 151 | return nil 152 | } 153 | out := new(RDBDatabaseStatus) 154 | in.DeepCopyInto(out) 155 | return out 156 | } 157 | 158 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 159 | func (in *RDBInstance) DeepCopyInto(out *RDBInstance) { 160 | *out = *in 161 | out.TypeMeta = in.TypeMeta 162 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 163 | in.Spec.DeepCopyInto(&out.Spec) 164 | in.Status.DeepCopyInto(&out.Status) 165 | } 166 | 167 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBInstance. 168 | func (in *RDBInstance) DeepCopy() *RDBInstance { 169 | if in == nil { 170 | return nil 171 | } 172 | out := new(RDBInstance) 173 | in.DeepCopyInto(out) 174 | return out 175 | } 176 | 177 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 178 | func (in *RDBInstance) DeepCopyObject() runtime.Object { 179 | if c := in.DeepCopy(); c != nil { 180 | return c 181 | } 182 | return nil 183 | } 184 | 185 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 186 | func (in *RDBInstanceAutoBackup) DeepCopyInto(out *RDBInstanceAutoBackup) { 187 | *out = *in 188 | if in.Frequency != nil { 189 | in, out := &in.Frequency, &out.Frequency 190 | *out = new(int32) 191 | **out = **in 192 | } 193 | if in.Retention != nil { 194 | in, out := &in.Retention, &out.Retention 195 | *out = new(int32) 196 | **out = **in 197 | } 198 | } 199 | 200 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBInstanceAutoBackup. 201 | func (in *RDBInstanceAutoBackup) DeepCopy() *RDBInstanceAutoBackup { 202 | if in == nil { 203 | return nil 204 | } 205 | out := new(RDBInstanceAutoBackup) 206 | in.DeepCopyInto(out) 207 | return out 208 | } 209 | 210 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 211 | func (in *RDBInstanceEndpoint) DeepCopyInto(out *RDBInstanceEndpoint) { 212 | *out = *in 213 | } 214 | 215 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBInstanceEndpoint. 216 | func (in *RDBInstanceEndpoint) DeepCopy() *RDBInstanceEndpoint { 217 | if in == nil { 218 | return nil 219 | } 220 | out := new(RDBInstanceEndpoint) 221 | in.DeepCopyInto(out) 222 | return out 223 | } 224 | 225 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 226 | func (in *RDBInstanceList) DeepCopyInto(out *RDBInstanceList) { 227 | *out = *in 228 | out.TypeMeta = in.TypeMeta 229 | in.ListMeta.DeepCopyInto(&out.ListMeta) 230 | if in.Items != nil { 231 | in, out := &in.Items, &out.Items 232 | *out = make([]RDBInstance, len(*in)) 233 | for i := range *in { 234 | (*in)[i].DeepCopyInto(&(*out)[i]) 235 | } 236 | } 237 | } 238 | 239 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBInstanceList. 240 | func (in *RDBInstanceList) DeepCopy() *RDBInstanceList { 241 | if in == nil { 242 | return nil 243 | } 244 | out := new(RDBInstanceList) 245 | in.DeepCopyInto(out) 246 | return out 247 | } 248 | 249 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 250 | func (in *RDBInstanceList) DeepCopyObject() runtime.Object { 251 | if c := in.DeepCopy(); c != nil { 252 | return c 253 | } 254 | return nil 255 | } 256 | 257 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 258 | func (in *RDBInstancePassword) DeepCopyInto(out *RDBInstancePassword) { 259 | *out = *in 260 | if in.Value != nil { 261 | in, out := &in.Value, &out.Value 262 | *out = new(string) 263 | **out = **in 264 | } 265 | if in.ValueFrom != nil { 266 | in, out := &in.ValueFrom, &out.ValueFrom 267 | *out = new(RDBInstancePasswordValueFrom) 268 | **out = **in 269 | } 270 | } 271 | 272 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBInstancePassword. 273 | func (in *RDBInstancePassword) DeepCopy() *RDBInstancePassword { 274 | if in == nil { 275 | return nil 276 | } 277 | out := new(RDBInstancePassword) 278 | in.DeepCopyInto(out) 279 | return out 280 | } 281 | 282 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 283 | func (in *RDBInstancePasswordValueFrom) DeepCopyInto(out *RDBInstancePasswordValueFrom) { 284 | *out = *in 285 | out.SecretKeyRef = in.SecretKeyRef 286 | } 287 | 288 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBInstancePasswordValueFrom. 289 | func (in *RDBInstancePasswordValueFrom) DeepCopy() *RDBInstancePasswordValueFrom { 290 | if in == nil { 291 | return nil 292 | } 293 | out := new(RDBInstancePasswordValueFrom) 294 | in.DeepCopyInto(out) 295 | return out 296 | } 297 | 298 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 299 | func (in *RDBInstanceRef) DeepCopyInto(out *RDBInstanceRef) { 300 | *out = *in 301 | } 302 | 303 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBInstanceRef. 304 | func (in *RDBInstanceRef) DeepCopy() *RDBInstanceRef { 305 | if in == nil { 306 | return nil 307 | } 308 | out := new(RDBInstanceRef) 309 | in.DeepCopyInto(out) 310 | return out 311 | } 312 | 313 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 314 | func (in *RDBInstanceSpec) DeepCopyInto(out *RDBInstanceSpec) { 315 | *out = *in 316 | if in.InstanceFrom != nil { 317 | in, out := &in.InstanceFrom, &out.InstanceFrom 318 | *out = new(RDBInstanceRef) 319 | **out = **in 320 | } 321 | if in.AutoBackup != nil { 322 | in, out := &in.AutoBackup, &out.AutoBackup 323 | *out = new(RDBInstanceAutoBackup) 324 | (*in).DeepCopyInto(*out) 325 | } 326 | if in.ACL != nil { 327 | in, out := &in.ACL, &out.ACL 328 | *out = new(RDBACL) 329 | (*in).DeepCopyInto(*out) 330 | } 331 | } 332 | 333 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBInstanceSpec. 334 | func (in *RDBInstanceSpec) DeepCopy() *RDBInstanceSpec { 335 | if in == nil { 336 | return nil 337 | } 338 | out := new(RDBInstanceSpec) 339 | in.DeepCopyInto(out) 340 | return out 341 | } 342 | 343 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 344 | func (in *RDBInstanceStatus) DeepCopyInto(out *RDBInstanceStatus) { 345 | *out = *in 346 | out.Endpoint = in.Endpoint 347 | in.Status.DeepCopyInto(&out.Status) 348 | } 349 | 350 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBInstanceStatus. 351 | func (in *RDBInstanceStatus) DeepCopy() *RDBInstanceStatus { 352 | if in == nil { 353 | return nil 354 | } 355 | out := new(RDBInstanceStatus) 356 | in.DeepCopyInto(out) 357 | return out 358 | } 359 | 360 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 361 | func (in *RDBPrivilege) DeepCopyInto(out *RDBPrivilege) { 362 | *out = *in 363 | } 364 | 365 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBPrivilege. 366 | func (in *RDBPrivilege) DeepCopy() *RDBPrivilege { 367 | if in == nil { 368 | return nil 369 | } 370 | out := new(RDBPrivilege) 371 | in.DeepCopyInto(out) 372 | return out 373 | } 374 | 375 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 376 | func (in *RDBUser) DeepCopyInto(out *RDBUser) { 377 | *out = *in 378 | out.TypeMeta = in.TypeMeta 379 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 380 | in.Spec.DeepCopyInto(&out.Spec) 381 | in.Status.DeepCopyInto(&out.Status) 382 | } 383 | 384 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBUser. 385 | func (in *RDBUser) DeepCopy() *RDBUser { 386 | if in == nil { 387 | return nil 388 | } 389 | out := new(RDBUser) 390 | in.DeepCopyInto(out) 391 | return out 392 | } 393 | 394 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 395 | func (in *RDBUser) DeepCopyObject() runtime.Object { 396 | if c := in.DeepCopy(); c != nil { 397 | return c 398 | } 399 | return nil 400 | } 401 | 402 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 403 | func (in *RDBUserList) DeepCopyInto(out *RDBUserList) { 404 | *out = *in 405 | out.TypeMeta = in.TypeMeta 406 | in.ListMeta.DeepCopyInto(&out.ListMeta) 407 | if in.Items != nil { 408 | in, out := &in.Items, &out.Items 409 | *out = make([]RDBUser, len(*in)) 410 | for i := range *in { 411 | (*in)[i].DeepCopyInto(&(*out)[i]) 412 | } 413 | } 414 | } 415 | 416 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBUserList. 417 | func (in *RDBUserList) DeepCopy() *RDBUserList { 418 | if in == nil { 419 | return nil 420 | } 421 | out := new(RDBUserList) 422 | in.DeepCopyInto(out) 423 | return out 424 | } 425 | 426 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 427 | func (in *RDBUserList) DeepCopyObject() runtime.Object { 428 | if c := in.DeepCopy(); c != nil { 429 | return c 430 | } 431 | return nil 432 | } 433 | 434 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 435 | func (in *RDBUserSpec) DeepCopyInto(out *RDBUserSpec) { 436 | *out = *in 437 | in.Password.DeepCopyInto(&out.Password) 438 | if in.Privileges != nil { 439 | in, out := &in.Privileges, &out.Privileges 440 | *out = make([]RDBPrivilege, len(*in)) 441 | copy(*out, *in) 442 | } 443 | out.InstanceRef = in.InstanceRef 444 | } 445 | 446 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBUserSpec. 447 | func (in *RDBUserSpec) DeepCopy() *RDBUserSpec { 448 | if in == nil { 449 | return nil 450 | } 451 | out := new(RDBUserSpec) 452 | in.DeepCopyInto(out) 453 | return out 454 | } 455 | 456 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 457 | func (in *RDBUserStatus) DeepCopyInto(out *RDBUserStatus) { 458 | *out = *in 459 | in.Status.DeepCopyInto(&out.Status) 460 | } 461 | 462 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RDBUserStatus. 463 | func (in *RDBUserStatus) DeepCopy() *RDBUserStatus { 464 | if in == nil { 465 | return nil 466 | } 467 | out := new(RDBUserStatus) 468 | in.DeepCopyInto(out) 469 | return out 470 | } 471 | -------------------------------------------------------------------------------- /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/rdb.scaleway.com_rdbdatabases.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.3.0 8 | creationTimestamp: null 9 | name: rdbdatabases.rdb.scaleway.com 10 | spec: 11 | group: rdb.scaleway.com 12 | names: 13 | kind: RDBDatabase 14 | listKind: RDBDatabaseList 15 | plural: rdbdatabases 16 | shortNames: 17 | - rdbd 18 | - rdbdatabase 19 | singular: rdbdatabase 20 | scope: Namespaced 21 | versions: 22 | - additionalPrinterColumns: 23 | - jsonPath: .status.size 24 | name: size 25 | type: string 26 | - jsonPath: .status.owner 27 | name: owner 28 | type: string 29 | - jsonPath: .status.managed 30 | name: managed 31 | type: string 32 | name: v1alpha1 33 | schema: 34 | openAPIV3Schema: 35 | description: RDBDatabase is the Schema for the rdbdatabases API 36 | properties: 37 | apiVersion: 38 | description: 'APIVersion defines the versioned schema of this representation 39 | of an object. Servers should convert recognized schemas to the latest 40 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 41 | type: string 42 | kind: 43 | description: 'Kind is a string value representing the REST resource this 44 | object represents. Servers may infer this from the endpoint the client 45 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 46 | type: string 47 | metadata: 48 | type: object 49 | spec: 50 | description: RDBDatabaseSpec defines the desired state of RDBDatabase 51 | properties: 52 | instanceRef: 53 | description: InstanceRef represents the reference to the instance 54 | of the database 55 | properties: 56 | externalID: 57 | description: ExternalID is the ID of the instance This field is 58 | immutable after creation 59 | type: string 60 | name: 61 | description: Name is the name of the instance of this database 62 | This field is immutable after creation 63 | type: string 64 | namespace: 65 | description: Namespace is the namespace of the instance of this 66 | database If empty, it will use the namespace of the database 67 | This field is immutable after creation 68 | type: string 69 | region: 70 | description: Region is the region of the instance This field is 71 | immutable after creation 72 | type: string 73 | type: object 74 | overrideName: 75 | description: OverrideName represents the name given to the database 76 | This field is immutable after creation 77 | type: string 78 | required: 79 | - instanceRef 80 | type: object 81 | status: 82 | description: RDBDatabaseStatus defines the observed state of RDBDatabase 83 | properties: 84 | conditions: 85 | items: 86 | description: Condition contains details for the current condition 87 | of this Scaleway resource. 88 | properties: 89 | lastProbeTime: 90 | description: Last time we probed the condition. 91 | format: date-time 92 | type: string 93 | lastTransitionTime: 94 | description: Last time the condition transitioned from one status 95 | to another. 96 | format: date-time 97 | type: string 98 | message: 99 | description: Human-readable message indicating details about 100 | last transition. 101 | type: string 102 | reason: 103 | description: Unique, one-word, CamelCase reason for the condition's 104 | last transition. 105 | type: string 106 | status: 107 | description: Status is the status of the condition. Can be True, 108 | False, Unknown. 109 | type: string 110 | type: 111 | description: Type is the type of the condition. 112 | type: string 113 | type: object 114 | type: array 115 | managed: 116 | description: Managed defines whether this database is mananged 117 | type: boolean 118 | owner: 119 | description: Owner represents the owner of this database 120 | type: string 121 | size: 122 | anyOf: 123 | - type: integer 124 | - type: string 125 | description: Size represents the size of the database 126 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 127 | x-kubernetes-int-or-string: true 128 | type: object 129 | type: object 130 | served: true 131 | storage: true 132 | subresources: 133 | status: {} 134 | status: 135 | acceptedNames: 136 | kind: "" 137 | plural: "" 138 | conditions: [] 139 | storedVersions: [] 140 | -------------------------------------------------------------------------------- /config/crd/bases/rdb.scaleway.com_rdbinstances.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.3.0 8 | creationTimestamp: null 9 | name: rdbinstances.rdb.scaleway.com 10 | spec: 11 | group: rdb.scaleway.com 12 | names: 13 | kind: RDBInstance 14 | listKind: RDBInstanceList 15 | plural: rdbinstances 16 | shortNames: 17 | - rdbi 18 | - rdbinstance 19 | singular: rdbinstance 20 | scope: Namespaced 21 | versions: 22 | - additionalPrinterColumns: 23 | - jsonPath: .status.endpoint.ip 24 | name: IP 25 | type: string 26 | - jsonPath: .status.endpoint.port 27 | name: Port 28 | type: integer 29 | name: v1alpha1 30 | schema: 31 | openAPIV3Schema: 32 | description: RDBInstance is the Schema for the databaseinstances API 33 | properties: 34 | apiVersion: 35 | description: 'APIVersion defines the versioned schema of this representation 36 | of an object. Servers should convert recognized schemas to the latest 37 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 38 | type: string 39 | kind: 40 | description: 'Kind is a string value representing the REST resource this 41 | object represents. Servers may infer this from the endpoint the client 42 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 43 | type: string 44 | metadata: 45 | type: object 46 | spec: 47 | description: RDBInstanceSpec defines the desired state of RDBInstance 48 | properties: 49 | acl: 50 | description: ACL represents the ACL rules of the RDBInstance 51 | properties: 52 | allowCluster: 53 | description: AllowCluster represents wether the nodes in the cluster 54 | should be allowed 55 | type: boolean 56 | rules: 57 | description: Rules represents the RDB ACL rules 58 | items: 59 | description: RDBACLRule defines a rule for a RDB ACL 60 | properties: 61 | description: 62 | description: Description is the description associated with 63 | this ACL rule 64 | type: string 65 | ipRange: 66 | description: IPRange represents a CIDR IP range 67 | type: string 68 | required: 69 | - ipRange 70 | type: object 71 | type: array 72 | type: object 73 | autoBackup: 74 | description: AutoBackup represents the RDBInstance auto backup policy 75 | properties: 76 | disabled: 77 | description: Disabled represents whether the auto backup should 78 | be disabled 79 | type: boolean 80 | frequency: 81 | description: Frequency represents the frequency, in hour, at which 82 | auto backups are made Default to 24 83 | format: int32 84 | minimum: 0 85 | type: integer 86 | retention: 87 | description: Retention represents the number of days the autobackup 88 | are kept Default to 7 89 | format: int32 90 | minimum: 0 91 | type: integer 92 | type: object 93 | engine: 94 | description: Engine is the database engine of the RDBInstance 95 | type: string 96 | instanceFrom: 97 | description: InstanceFrom allows to create an instance from an existing 98 | one At most one of InstanceID/Region and InstanceFrom have to be 99 | specified on creation. This field is immutable after creation 100 | properties: 101 | externalID: 102 | description: ExternalID is the ID of the instance This field is 103 | immutable after creation 104 | type: string 105 | name: 106 | description: Name is the name of the instance of this database 107 | This field is immutable after creation 108 | type: string 109 | namespace: 110 | description: Namespace is the namespace of the instance of this 111 | database If empty, it will use the namespace of the database 112 | This field is immutable after creation 113 | type: string 114 | region: 115 | description: Region is the region of the instance This field is 116 | immutable after creation 117 | type: string 118 | type: object 119 | instanceID: 120 | description: InstanceID is the ID of the instance If empty it will 121 | create a new instance If set it will use this ID as the instance 122 | ID This field is immutable after creation At most one of InstanceID/Region 123 | and InstanceFrom have to be specified on creation. 124 | type: string 125 | isHaCluster: 126 | description: IsHaCluster represents whether the RDBInstance should 127 | be in HA mode Defaults to false 128 | type: boolean 129 | nodeType: 130 | description: NodeType is the type of node to use for the RDBInstance 131 | type: string 132 | region: 133 | description: Region is the region in which the RDBInstance will run 134 | This field is immutable after creation Defaults to the controller 135 | default region At most one of InstanceID/Region and InstanceFrom 136 | have to be specified on creation. 137 | type: string 138 | required: 139 | - engine 140 | - nodeType 141 | type: object 142 | status: 143 | description: RDBInstanceStatus defines the observed state of RDBInstance 144 | properties: 145 | conditions: 146 | items: 147 | description: Condition contains details for the current condition 148 | of this Scaleway resource. 149 | properties: 150 | lastProbeTime: 151 | description: Last time we probed the condition. 152 | format: date-time 153 | type: string 154 | lastTransitionTime: 155 | description: Last time the condition transitioned from one status 156 | to another. 157 | format: date-time 158 | type: string 159 | message: 160 | description: Human-readable message indicating details about 161 | last transition. 162 | type: string 163 | reason: 164 | description: Unique, one-word, CamelCase reason for the condition's 165 | last transition. 166 | type: string 167 | status: 168 | description: Status is the status of the condition. Can be True, 169 | False, Unknown. 170 | type: string 171 | type: 172 | description: Type is the type of the condition. 173 | type: string 174 | type: object 175 | type: array 176 | endpoint: 177 | description: Endpoint is the endpoint of the RDBInstance 178 | properties: 179 | ip: 180 | description: IP is the IP of the RDBInstance 181 | type: string 182 | port: 183 | description: Port if the port of the RDBInstance 184 | format: int32 185 | type: integer 186 | type: object 187 | type: object 188 | type: object 189 | served: true 190 | storage: true 191 | subresources: 192 | status: {} 193 | status: 194 | acceptedNames: 195 | kind: "" 196 | plural: "" 197 | conditions: [] 198 | storedVersions: [] 199 | -------------------------------------------------------------------------------- /config/crd/bases/rdb.scaleway.com_rdbusers.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.3.0 8 | creationTimestamp: null 9 | name: rdbusers.rdb.scaleway.com 10 | spec: 11 | group: rdb.scaleway.com 12 | names: 13 | kind: RDBUser 14 | listKind: RDBUserList 15 | plural: rdbusers 16 | shortNames: 17 | - rdbu 18 | - rdbuser 19 | singular: rdbuser 20 | scope: Namespaced 21 | versions: 22 | - additionalPrinterColumns: 23 | - jsonPath: .spec.userName 24 | name: UserName 25 | type: string 26 | name: v1alpha1 27 | schema: 28 | openAPIV3Schema: 29 | description: RDBUser is the Schema for the rdbusers API 30 | properties: 31 | apiVersion: 32 | description: 'APIVersion defines the versioned schema of this representation 33 | of an object. Servers should convert recognized schemas to the latest 34 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' 35 | type: string 36 | kind: 37 | description: 'Kind is a string value representing the REST resource this 38 | object represents. Servers may infer this from the endpoint the client 39 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' 40 | type: string 41 | metadata: 42 | type: object 43 | spec: 44 | description: RDBUserSpec defines the desired state of RDBUser 45 | properties: 46 | admin: 47 | description: Admin represents whether the user is an admin user 48 | type: boolean 49 | instanceRef: 50 | description: InstanceRef represents the reference to the instance 51 | of the user 52 | properties: 53 | externalID: 54 | description: ExternalID is the ID of the instance This field is 55 | immutable after creation 56 | type: string 57 | name: 58 | description: Name is the name of the instance of this database 59 | This field is immutable after creation 60 | type: string 61 | namespace: 62 | description: Namespace is the namespace of the instance of this 63 | database If empty, it will use the namespace of the database 64 | This field is immutable after creation 65 | type: string 66 | region: 67 | description: Region is the region of the instance This field is 68 | immutable after creation 69 | type: string 70 | type: object 71 | password: 72 | description: Password is the password associated to the user 73 | properties: 74 | value: 75 | description: Value represents a raw value 76 | type: string 77 | valueFrom: 78 | description: ValueFrom represents a value from a secret 79 | properties: 80 | secretKeyRef: 81 | description: SecretReference represents a Secret Reference. 82 | It has enough information to retrieve secret in any namespace 83 | properties: 84 | name: 85 | description: Name is unique within a namespace to reference 86 | a secret resource. 87 | type: string 88 | namespace: 89 | description: Namespace defines the space within which 90 | the secret name must be unique. 91 | type: string 92 | type: object 93 | required: 94 | - secretKeyRef 95 | type: object 96 | type: object 97 | privileges: 98 | description: Privileges represents the privileges given to this user 99 | items: 100 | description: RDBPrivilege defines a privilege linked to a RDBUser 101 | properties: 102 | databaseRef: 103 | description: DatabaseName is the name to a RDB Database for 104 | this privilege 105 | type: string 106 | permission: 107 | description: Permission is the given permission for this privilege 108 | enum: 109 | - ReadOnly 110 | - ReadWrite 111 | - All 112 | - None 113 | type: string 114 | required: 115 | - databaseRef 116 | - permission 117 | type: object 118 | type: array 119 | userName: 120 | description: UserName is the user name to be created on the RDBInstance 121 | type: string 122 | required: 123 | - instanceRef 124 | - password 125 | - userName 126 | type: object 127 | status: 128 | description: RDBUserStatus defines the observed state of RDBUser 129 | properties: 130 | conditions: 131 | items: 132 | description: Condition contains details for the current condition 133 | of this Scaleway resource. 134 | properties: 135 | lastProbeTime: 136 | description: Last time we probed the condition. 137 | format: date-time 138 | type: string 139 | lastTransitionTime: 140 | description: Last time the condition transitioned from one status 141 | to another. 142 | format: date-time 143 | type: string 144 | message: 145 | description: Human-readable message indicating details about 146 | last transition. 147 | type: string 148 | reason: 149 | description: Unique, one-word, CamelCase reason for the condition's 150 | last transition. 151 | type: string 152 | status: 153 | description: Status is the status of the condition. Can be True, 154 | False, Unknown. 155 | type: string 156 | type: 157 | description: Type is the type of the condition. 158 | type: string 159 | type: object 160 | type: array 161 | type: object 162 | type: object 163 | served: true 164 | storage: true 165 | subresources: 166 | status: {} 167 | status: 168 | acceptedNames: 169 | kind: "" 170 | plural: "" 171 | conditions: [] 172 | storedVersions: [] 173 | -------------------------------------------------------------------------------- /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/rdb.scaleway.com_rdbinstances.yaml 6 | - bases/rdb.scaleway.com_rdbdatabases.yaml 7 | - bases/rdb.scaleway.com_rdbusers.yaml 8 | # +kubebuilder:scaffold:crdkustomizeresource 9 | 10 | patchesStrategicMerge: 11 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 12 | # patches here are for enabling the conversion webhook for each CRD 13 | #- patches/webhook_in_rdbinstances.yaml 14 | #- patches/webhook_in_rdbdatabases.yaml 15 | #- patches/webhook_in_rdbusers.yaml 16 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 17 | 18 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 19 | # patches here are for enabling the CA injection for each CRD 20 | - patches/cainjection_in_rdbinstances.yaml 21 | - patches/cainjection_in_rdbdatabases.yaml 22 | #- patches/cainjection_in_rdbusers.yaml 23 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 24 | 25 | # the following config is for teaching kustomize how to do kustomization for CRDs. 26 | configurations: 27 | - kustomizeconfig.yaml 28 | -------------------------------------------------------------------------------- /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_rdbdatabases.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/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: rdbdatabases.rdb.scaleway.com 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_rdbinstances.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/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: rdbinstances.rdb.scaleway.com 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_rdbusers.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: rdbusers.rdb.scaleway.com 9 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_rdbdatabases.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: rdbdatabases.rdb.scaleway.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhook: 11 | clientConfig: 12 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 13 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 14 | caBundle: Cg== 15 | service: 16 | namespace: system 17 | name: webhook-service 18 | path: /convert 19 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_rdbinstances.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: rdbinstances.rdb.scaleway.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhook: 11 | clientConfig: 12 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 13 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 14 | caBundle: Cg== 15 | service: 16 | namespace: system 17 | name: webhook-service 18 | path: /convert 19 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_rdbusers.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: rdbusers.rdb.scaleway.com 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: scaleway-operator-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: scaleway-operator- 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/v1 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 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: scaleway/scaleway-operator 8 | -------------------------------------------------------------------------------- /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 | - /scaleway-operator 28 | args: 29 | - --enable-leader-election 30 | image: controller 31 | name: manager 32 | env: 33 | - name: SCW_ACCESS_KEY 34 | valueFrom: 35 | secretKeyRef: 36 | name: scaleway-operator-secrets 37 | key: SCW_ACCESS_KEY 38 | - name: SCW_SECRET_KEY 39 | valueFrom: 40 | secretKeyRef: 41 | name: scaleway-operator-secrets 42 | key: SCW_SECRET_KEY 43 | - name: SCW_DEFAULT_REGION 44 | valueFrom: 45 | secretKeyRef: 46 | name: scaleway-operator-secrets 47 | key: SCW_DEFAULT_REGION 48 | - name: SCW_DEFAULT_PROJECT_ID 49 | valueFrom: 50 | secretKeyRef: 51 | name: scaleway-operator-secrets 52 | key: SCW_DEFAULT_PROJECT_ID 53 | resources: 54 | limits: 55 | cpu: 100m 56 | memory: 30Mi 57 | requests: 58 | cpu: 100m 59 | memory: 20Mi 60 | terminationGracePeriodSeconds: 10 61 | -------------------------------------------------------------------------------- /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/rdbdatabase_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit rdbdatabases. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: rdbdatabase-editor-role 6 | rules: 7 | - apiGroups: 8 | - rdb.scaleway.com 9 | resources: 10 | - rdbdatabases 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - rdb.scaleway.com 21 | resources: 22 | - rdbdatabases/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/rdbdatabase_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view rdbdatabases. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: rdbdatabase-viewer-role 6 | rules: 7 | - apiGroups: 8 | - rdb.scaleway.com 9 | resources: 10 | - rdbdatabases 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - rdb.scaleway.com 17 | resources: 18 | - rdbdatabases/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/rdbinstance_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit rdbinstances. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: rdbinstance-editor-role 6 | rules: 7 | - apiGroups: 8 | - rdb.scaleway.com 9 | resources: 10 | - rdbinstances 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - rdb.scaleway.com 21 | resources: 22 | - rdbinstances/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/rdbinstance_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view rdbinstances. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: rdbinstance-viewer-role 6 | rules: 7 | - apiGroups: 8 | - rdb.scaleway.com 9 | resources: 10 | - rdbinstances 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - rdb.scaleway.com 17 | resources: 18 | - rdbinstances/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/rdbuser_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit rdbusers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: rdbuser-editor-role 6 | rules: 7 | - apiGroups: 8 | - rdb.scaleway.com 9 | resources: 10 | - rdbusers 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - rdb.scaleway.com 21 | resources: 22 | - rdbusers/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/rdbuser_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view rdbusers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: rdbuser-viewer-role 6 | rules: 7 | - apiGroups: 8 | - rdb.scaleway.com 9 | resources: 10 | - rdbusers 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - rdb.scaleway.com 17 | resources: 18 | - rdbusers/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /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 | - "" 11 | resources: 12 | - events 13 | verbs: 14 | - create 15 | - patch 16 | - apiGroups: 17 | - "" 18 | resources: 19 | - nodes 20 | verbs: 21 | - get 22 | - list 23 | - watch 24 | - apiGroups: 25 | - rdb.scaleway.com 26 | resources: 27 | - rdbdatabases 28 | verbs: 29 | - create 30 | - delete 31 | - get 32 | - list 33 | - patch 34 | - update 35 | - watch 36 | - apiGroups: 37 | - rdb.scaleway.com 38 | resources: 39 | - rdbdatabases/status 40 | verbs: 41 | - get 42 | - patch 43 | - update 44 | - apiGroups: 45 | - rdb.scaleway.com 46 | resources: 47 | - rdbinstances 48 | verbs: 49 | - create 50 | - delete 51 | - get 52 | - list 53 | - patch 54 | - update 55 | - watch 56 | - apiGroups: 57 | - rdb.scaleway.com 58 | resources: 59 | - rdbinstances/status 60 | verbs: 61 | - get 62 | - patch 63 | - update 64 | - apiGroups: 65 | - rdb.scaleway.com 66 | resources: 67 | - rdbusers 68 | verbs: 69 | - create 70 | - delete 71 | - get 72 | - list 73 | - patch 74 | - update 75 | - watch 76 | - apiGroups: 77 | - rdb.scaleway.com 78 | resources: 79 | - rdbusers/status 80 | verbs: 81 | - get 82 | - patch 83 | - update 84 | -------------------------------------------------------------------------------- /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/samples/rdb_v1alpha1_rdbdatabase.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rdb.scaleway.com/v1alpha1 2 | kind: RDBDatabase 3 | metadata: 4 | name: rdbdatabase-sample 5 | spec: 6 | instanceRef: 7 | name: myawsomedb 8 | -------------------------------------------------------------------------------- /config/samples/rdb_v1alpha1_rdbinstance.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rdb.scaleway.com/v1alpha1 2 | kind: RDBInstance 3 | metadata: 4 | name: myawsomedbbismysqld 5 | spec: 6 | engine: MySQL-8 7 | region: nl-ams 8 | nodeType: db-dev-m 9 | -------------------------------------------------------------------------------- /config/samples/rdb_v1alpha1_rdbuser.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rdb.scaleway.com/v1alpha1 2 | kind: RDBUser 3 | metadata: 4 | name: rdbuser-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: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: admissionregistration.k8s.io/v1beta1 4 | kind: ValidatingWebhookConfiguration 5 | metadata: 6 | creationTimestamp: null 7 | name: validating-webhook-configuration 8 | webhooks: 9 | - clientConfig: 10 | caBundle: Cg== 11 | service: 12 | name: webhook-service 13 | namespace: system 14 | path: /validate-rdb-scaleway-com-v1alpha1-rdbdatabase 15 | failurePolicy: Fail 16 | name: vrdbdatabase.kb.io 17 | rules: 18 | - apiGroups: 19 | - rdb.scaleway.com 20 | apiVersions: 21 | - v1alpha1 22 | operations: 23 | - CREATE 24 | - UPDATE 25 | resources: 26 | - rdbdatabases 27 | - clientConfig: 28 | caBundle: Cg== 29 | service: 30 | name: webhook-service 31 | namespace: system 32 | path: /validate-rdb-scaleway-com-v1alpha1-rdbinstance 33 | failurePolicy: Fail 34 | name: vrdbinstance.kb.io 35 | rules: 36 | - apiGroups: 37 | - rdb.scaleway.com 38 | apiVersions: 39 | - v1alpha1 40 | operations: 41 | - CREATE 42 | - UPDATE 43 | resources: 44 | - rdbinstances 45 | -------------------------------------------------------------------------------- /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/rdb/rdbdatabase_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 | ctrl "sigs.k8s.io/controller-runtime" 21 | 22 | rdbv1alpha1 "github.com/scaleway/scaleway-operator/apis/rdb/v1alpha1" 23 | "github.com/scaleway/scaleway-operator/controllers" 24 | ) 25 | 26 | // RDBDatabaseReconciler reconciles a RDBDatabase object 27 | type RDBDatabaseReconciler struct { 28 | ScalewayReconciler *controllers.ScalewayReconciler 29 | } 30 | 31 | // +kubebuilder:rbac:groups=rdb.scaleway.com,resources=rdbdatabases,verbs=get;list;watch;create;update;patch;delete 32 | // +kubebuilder:rbac:groups=rdb.scaleway.com,resources=rdbdatabases/status,verbs=get;update;patch 33 | // +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch 34 | 35 | // Reconcile reconsiles the RDB Database 36 | func (r *RDBDatabaseReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { 37 | return r.ScalewayReconciler.Reconcile(req, &rdbv1alpha1.RDBDatabase{}) 38 | } 39 | 40 | // SetupWithManager registers the RDB Database controller 41 | func (r *RDBDatabaseReconciler) SetupWithManager(mgr ctrl.Manager) error { 42 | return ctrl.NewControllerManagedBy(mgr). 43 | For(&rdbv1alpha1.RDBDatabase{}). 44 | Complete(r) 45 | } 46 | -------------------------------------------------------------------------------- /controllers/rdb/rdbinstance_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 | ctrl "sigs.k8s.io/controller-runtime" 21 | 22 | rdbv1alpha1 "github.com/scaleway/scaleway-operator/apis/rdb/v1alpha1" 23 | "github.com/scaleway/scaleway-operator/controllers" 24 | ) 25 | 26 | // RDBInstanceReconciler reconciles a RDBInstance object 27 | type RDBInstanceReconciler struct { 28 | ScalewayReconciler *controllers.ScalewayReconciler 29 | } 30 | 31 | // +kubebuilder:rbac:groups=rdb.scaleway.com,resources=rdbinstances,verbs=get;list;watch;create;update;patch;delete 32 | // +kubebuilder:rbac:groups=rdb.scaleway.com,resources=rdbinstances/status,verbs=get;update;patch 33 | // +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch 34 | // +kubebuilder:rbac:groups=core,resources=nodes,verbs=get;list;watch 35 | 36 | // Reconcile reconciles the RDB Instance 37 | func (r *RDBInstanceReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { 38 | return r.ScalewayReconciler.Reconcile(req, &rdbv1alpha1.RDBInstance{}) 39 | } 40 | 41 | // SetupWithManager registers the RDB Instance Controller 42 | func (r *RDBInstanceReconciler) SetupWithManager(mgr ctrl.Manager) error { 43 | return ctrl.NewControllerManagedBy(mgr). 44 | For(&rdbv1alpha1.RDBInstance{}). 45 | Complete(r) 46 | } 47 | -------------------------------------------------------------------------------- /controllers/rdb/rdbuser_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 | ctrl "sigs.k8s.io/controller-runtime" 21 | 22 | rdbv1alpha1 "github.com/scaleway/scaleway-operator/apis/rdb/v1alpha1" 23 | "github.com/scaleway/scaleway-operator/controllers" 24 | ) 25 | 26 | // RDBUserReconciler reconciles a RDBUser object 27 | type RDBUserReconciler struct { 28 | ScalewayReconciler *controllers.ScalewayReconciler 29 | } 30 | 31 | // +kubebuilder:rbac:groups=rdb.scaleway.com,resources=rdbusers,verbs=get;list;watch;create;update;patch;delete 32 | // +kubebuilder:rbac:groups=rdb.scaleway.com,resources=rdbusers/status,verbs=get;update;patch 33 | // +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch 34 | 35 | // Reconcile reconciles a RDB User 36 | func (r *RDBUserReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { 37 | return r.ScalewayReconciler.Reconcile(req, &rdbv1alpha1.RDBUser{}) 38 | } 39 | 40 | // SetupWithManager registers the RDB User controller 41 | func (r *RDBUserReconciler) SetupWithManager(mgr ctrl.Manager) error { 42 | return ctrl.NewControllerManagedBy(mgr). 43 | For(&rdbv1alpha1.RDBUser{}). 44 | Complete(r) 45 | } 46 | -------------------------------------------------------------------------------- /controllers/scaleway_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | "github.com/go-logr/logr" 9 | scalewaymetav1alpha1 "github.com/scaleway/scaleway-operator/apis/meta/v1alpha1" 10 | "github.com/scaleway/scaleway-operator/pkg/manager/scaleway" 11 | "github.com/scaleway/scaleway-sdk-go/scw" 12 | corev1 "k8s.io/api/core/v1" 13 | "k8s.io/apimachinery/pkg/api/meta" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | "k8s.io/client-go/tools/record" 17 | ctrl "sigs.k8s.io/controller-runtime" 18 | "sigs.k8s.io/controller-runtime/pkg/client" 19 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 20 | ) 21 | 22 | const ( 23 | finalizerName = "scaleway.com/finalizer" 24 | ignoreAnnotation = "scaleway.com/ignore" 25 | 26 | messageStillReconciling = "Still reconciling" 27 | 28 | reasonReconciling = "Reconciling" 29 | reasonTransientState = "TransientState" 30 | reasonResourceNotFound = "ResourceNotFound" 31 | reasonPermissionsDenied = "PermissionsDenied" 32 | reasonOutOfStock = "OutOfStock" 33 | reasonQuotasExceeded = "QuotasExceeded" 34 | reasonResourceLocked = "ResourceLocked" 35 | reasonInvalidArguments = "InvalidArguments" 36 | ) 37 | 38 | var ( 39 | // RequeueDuration is the default requeue duration 40 | // It is exported to reduce it when running tests with existing data 41 | RequeueDuration time.Duration = time.Second * 30 42 | ) 43 | 44 | // ScalewayReconciler is the base reconciler for Scaleway products 45 | type ScalewayReconciler struct { 46 | client.Client 47 | ScalewayManager scaleway.Manager 48 | Recorder record.EventRecorder 49 | Log logr.Logger 50 | Scheme *runtime.Scheme 51 | } 52 | 53 | // Reconcile is the global reconcile loop 54 | func (r *ScalewayReconciler) Reconcile(req ctrl.Request, obj runtime.Object) (ctrl.Result, error) { 55 | ctx := context.Background() 56 | log := r.Log.WithValues("object", req.String()) 57 | 58 | if err := r.Get(ctx, req.NamespacedName, obj); err != nil { 59 | log.Error(err, "could not find object") 60 | return ctrl.Result{}, client.IgnoreNotFound(err) 61 | } 62 | 63 | objMeta, err := meta.Accessor(obj) 64 | if err != nil { 65 | log.Error(err, "failed to get object meta") 66 | return ctrl.Result{}, err 67 | } 68 | 69 | if ignore, ok := objMeta.GetAnnotations()[ignoreAnnotation]; ok { 70 | if strings.ToLower(ignore) == "true" { 71 | if !objMeta.GetDeletionTimestamp().IsZero() { 72 | controllerutil.RemoveFinalizer(obj.(controllerutil.Object), finalizerName) 73 | } 74 | r.Recorder.Event(obj, corev1.EventTypeNormal, "Ignoring", "Ignoring object based on annotation") 75 | return ctrl.Result{}, r.Update(ctx, obj) 76 | } 77 | } 78 | 79 | if objMeta.GetDeletionTimestamp().IsZero() { 80 | if !controllerutil.ContainsFinalizer(obj.(controllerutil.Object), finalizerName) { 81 | controllerutil.AddFinalizer(obj.(controllerutil.Object), finalizerName) 82 | if err := r.Update(ctx, obj); err != nil { 83 | log.Error(err, "failed to add finalizer") 84 | return ctrl.Result{}, err 85 | } 86 | } 87 | } else { 88 | // deletion 89 | if controllerutil.ContainsFinalizer(obj.(controllerutil.Object), finalizerName) { 90 | deleted, err := r.ScalewayManager.Delete(ctx, obj) 91 | if err != nil { 92 | log.Error(err, "failed to delete") 93 | return ctrl.Result{}, err 94 | } 95 | if deleted { 96 | r.Recorder.Event(obj, corev1.EventTypeNormal, "Deleted", "Successfully deleted") 97 | controllerutil.RemoveFinalizer(obj.(controllerutil.Object), finalizerName) 98 | return ctrl.Result{}, r.Update(ctx, obj) 99 | } 100 | log.Info("still deleting") 101 | return ctrl.Result{RequeueAfter: RequeueDuration}, r.Status().Update(ctx, obj) 102 | } 103 | return ctrl.Result{}, nil 104 | } 105 | 106 | err = r.setOwners(ctx, log, obj, objMeta) 107 | if err != nil { 108 | return ctrl.Result{}, err 109 | } 110 | 111 | log.Info("reconciling object") 112 | 113 | ensured, err := r.ScalewayManager.Ensure(ctx, obj) 114 | if err != nil { 115 | log.Error(err, "error ensuring object") 116 | } 117 | 118 | scalewayStatus := obj.(scalewaymetav1alpha1.TypeMeta).GetStatus() 119 | requeueAfter, updateErr := updateStatus(&scalewayStatus, metav1.NewTime(time.Now()), err, ensured) 120 | obj.(scalewaymetav1alpha1.TypeMeta).SetStatus(scalewayStatus) 121 | err = r.Status().Update(ctx, obj) 122 | if err != nil { 123 | log.Error(err, "failed to update status") 124 | return ctrl.Result{}, err 125 | } 126 | 127 | return ctrl.Result{RequeueAfter: requeueAfter}, updateErr 128 | } 129 | 130 | func (r *ScalewayReconciler) setOwners(ctx context.Context, log logr.Logger, obj runtime.Object, objMeta metav1.Object) error { 131 | owners, err := r.ScalewayManager.GetOwners(ctx, obj) 132 | if err != nil { 133 | log.Error(err, "failed to get owners") 134 | return err 135 | } 136 | 137 | for _, owner := range owners { 138 | if err := r.Get(ctx, owner.Key, owner.Object); err == nil { 139 | if ownerMeta, err := meta.Accessor(owner.Object); err == nil { 140 | err = controllerutil.SetControllerReference(ownerMeta, objMeta, r.Scheme) 141 | if err != nil { 142 | log.Error(err, "failed to set controller reference") 143 | continue 144 | } 145 | err := r.Update(ctx, obj) 146 | if err != nil { 147 | log.Error(err, "failed to update controller reference") 148 | continue 149 | } 150 | log.Info("controller reference set") 151 | break 152 | } 153 | } 154 | } 155 | 156 | return nil 157 | } 158 | 159 | func updateStatus(status *scalewaymetav1alpha1.Status, now metav1.Time, ensureErr error, reconciled bool) (time.Duration, error) { 160 | reconcileStatus := corev1.ConditionTrue 161 | var message string 162 | var reason string 163 | 164 | var requeueAfter time.Duration 165 | 166 | if ensureErr != nil { 167 | message = ensureErr.Error() 168 | reconcileStatus = corev1.ConditionFalse 169 | 170 | switch ensureErr.(type) { 171 | case *scw.ResourceNotFoundError: 172 | reason = reasonResourceNotFound 173 | ensureErr = nil 174 | case *scw.InvalidArgumentsError: 175 | reason = reasonInvalidArguments 176 | ensureErr = nil 177 | case *scw.PermissionsDeniedError: 178 | reason = reasonPermissionsDenied 179 | requeueAfter = RequeueDuration * 10 180 | ensureErr = nil 181 | case *scw.OutOfStockError: 182 | reason = reasonOutOfStock 183 | requeueAfter = RequeueDuration * 4 184 | ensureErr = nil 185 | case *scw.QuotasExceededError: 186 | reason = reasonQuotasExceeded 187 | requeueAfter = RequeueDuration * 2 188 | ensureErr = nil 189 | case *scw.ResourceLockedError: 190 | reason = reasonResourceLocked 191 | requeueAfter = RequeueDuration * 10 192 | ensureErr = nil 193 | case *scw.TransientStateError: 194 | reason = reasonTransientState 195 | requeueAfter = RequeueDuration 196 | ensureErr = nil 197 | } 198 | 199 | } else { 200 | if !reconciled { 201 | requeueAfter = RequeueDuration 202 | reconcileStatus = corev1.ConditionFalse 203 | message = messageStillReconciling 204 | reason = reasonReconciling 205 | } 206 | } 207 | 208 | updateCondition(status, scalewaymetav1alpha1.Condition{ 209 | Message: message, 210 | Reason: reason, 211 | Status: reconcileStatus, 212 | Type: scalewaymetav1alpha1.Reconciled, 213 | }, now) 214 | 215 | return requeueAfter, ensureErr 216 | } 217 | 218 | func updateCondition(status *scalewaymetav1alpha1.Status, condition scalewaymetav1alpha1.Condition, now metav1.Time) { 219 | for i, c := range status.Conditions { 220 | if c.Type == condition.Type { 221 | cond := &status.Conditions[i] 222 | cond.LastProbeTime = now 223 | cond.Message = condition.Message 224 | cond.Reason = condition.Reason 225 | if cond.Status != condition.Status { 226 | cond.LastTransitionTime = now 227 | } 228 | cond.Status = condition.Status 229 | return 230 | } 231 | } 232 | status.Conditions = append(status.Conditions, scalewaymetav1alpha1.Condition{ 233 | Type: condition.Type, 234 | LastProbeTime: now, 235 | LastTransitionTime: now, 236 | Message: condition.Message, 237 | Reason: condition.Reason, 238 | Status: condition.Status, 239 | }) 240 | } 241 | -------------------------------------------------------------------------------- /controllers/scaleway_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | scalewaymetav1alpha1 "github.com/scaleway/scaleway-operator/apis/meta/v1alpha1" 8 | "github.com/scaleway/scaleway-sdk-go/scw" 9 | corev1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | func compareStatus(s1, s2 *scalewaymetav1alpha1.Status) bool { 14 | if len(s1.Conditions) != len(s2.Conditions) { 15 | return false 16 | } 17 | for i := range s1.Conditions { 18 | c1 := s1.Conditions[i] 19 | c2 := s2.Conditions[i] 20 | if !c1.LastProbeTime.Equal(&c2.LastProbeTime) || 21 | !c1.LastTransitionTime.Equal(&c2.LastTransitionTime) || 22 | c1.Message != c2.Message || 23 | c1.Reason != c2.Reason || 24 | c1.Status != c2.Status || 25 | c1.Type != c2.Type { 26 | return false 27 | } 28 | } 29 | return true 30 | } 31 | 32 | func Test_updateStatus(t *testing.T) { 33 | now := metav1.NewTime(time.Now()) 34 | before := metav1.NewTime(time.Now().Add(-5 * time.Second)) 35 | 36 | notFoundErr := &scw.ResourceNotFoundError{ 37 | Resource: "dummy", 38 | ResourceID: "dummyID", 39 | } 40 | 41 | tStateErr := &scw.TransientStateError{ 42 | CurrentState: "dummy-state", 43 | Resource: "dummy", 44 | ResourceID: "dummyID", 45 | } 46 | 47 | permDeniedErr := &scw.PermissionsDeniedError{} 48 | 49 | oosErr := &scw.OutOfStockError{ 50 | Resource: "dummy", 51 | } 52 | 53 | quotasExceededError := &scw.QuotasExceededError{ 54 | Details: []scw.QuotasExceededErrorDetail{ 55 | { 56 | Resource: "dummy", 57 | Current: 2, 58 | Quota: 3, 59 | }, 60 | }, 61 | } 62 | 63 | resLockedErr := &scw.ResourceLockedError{ 64 | Resource: "dummy", 65 | ResourceID: "dummyID", 66 | } 67 | 68 | invalidArgErr := &scw.InvalidArgumentsError{ 69 | Details: []scw.InvalidArgumentsErrorDetail{ 70 | { 71 | ArgumentName: "dummy-arg", 72 | HelpMessage: "I NEED SOMEBODY! HELP! NOT JUST ANYBODY", 73 | Reason: "I'm feeling down", 74 | }, 75 | }, 76 | } 77 | 78 | cases := []struct { 79 | status *scalewaymetav1alpha1.Status 80 | ensureErr error 81 | reconciled bool 82 | err error 83 | newStatus *scalewaymetav1alpha1.Status 84 | }{ 85 | { 86 | &scalewaymetav1alpha1.Status{}, 87 | nil, 88 | true, 89 | nil, 90 | &scalewaymetav1alpha1.Status{ 91 | Conditions: []scalewaymetav1alpha1.Condition{ 92 | { 93 | Type: scalewaymetav1alpha1.Reconciled, 94 | LastProbeTime: now, 95 | LastTransitionTime: now, 96 | Message: "", 97 | Reason: "", 98 | Status: corev1.ConditionTrue, 99 | }, 100 | }, 101 | }, 102 | }, 103 | { 104 | &scalewaymetav1alpha1.Status{}, 105 | nil, 106 | false, 107 | nil, 108 | &scalewaymetav1alpha1.Status{ 109 | Conditions: []scalewaymetav1alpha1.Condition{ 110 | { 111 | Type: scalewaymetav1alpha1.Reconciled, 112 | LastProbeTime: now, 113 | LastTransitionTime: now, 114 | Message: messageStillReconciling, 115 | Reason: reasonReconciling, 116 | Status: corev1.ConditionFalse, 117 | }, 118 | }, 119 | }, 120 | }, 121 | { 122 | &scalewaymetav1alpha1.Status{ 123 | Conditions: []scalewaymetav1alpha1.Condition{ 124 | { 125 | Type: scalewaymetav1alpha1.Reconciled, 126 | LastProbeTime: before, 127 | LastTransitionTime: before, 128 | Message: messageStillReconciling, 129 | Reason: reasonReconciling, 130 | Status: corev1.ConditionFalse, 131 | }, 132 | }, 133 | }, 134 | nil, 135 | true, 136 | nil, 137 | &scalewaymetav1alpha1.Status{ 138 | Conditions: []scalewaymetav1alpha1.Condition{ 139 | { 140 | Type: scalewaymetav1alpha1.Reconciled, 141 | LastProbeTime: now, 142 | LastTransitionTime: now, 143 | Message: "", 144 | Reason: "", 145 | Status: corev1.ConditionTrue, 146 | }, 147 | }, 148 | }, 149 | }, 150 | { 151 | &scalewaymetav1alpha1.Status{ 152 | Conditions: []scalewaymetav1alpha1.Condition{ 153 | { 154 | Type: scalewaymetav1alpha1.Reconciled, 155 | LastProbeTime: before, 156 | LastTransitionTime: before, 157 | Message: messageStillReconciling, 158 | Reason: reasonReconciling, 159 | Status: corev1.ConditionFalse, 160 | }, 161 | }, 162 | }, 163 | nil, 164 | false, 165 | nil, 166 | &scalewaymetav1alpha1.Status{ 167 | Conditions: []scalewaymetav1alpha1.Condition{ 168 | { 169 | Type: scalewaymetav1alpha1.Reconciled, 170 | LastProbeTime: now, 171 | LastTransitionTime: before, 172 | Message: messageStillReconciling, 173 | Reason: reasonReconciling, 174 | Status: corev1.ConditionFalse, 175 | }, 176 | }, 177 | }, 178 | }, 179 | { 180 | &scalewaymetav1alpha1.Status{ 181 | Conditions: []scalewaymetav1alpha1.Condition{ 182 | { 183 | Type: scalewaymetav1alpha1.Reconciled, 184 | LastProbeTime: before, 185 | LastTransitionTime: before, 186 | Message: messageStillReconciling, 187 | Reason: reasonReconciling, 188 | Status: corev1.ConditionFalse, 189 | }, 190 | }, 191 | }, 192 | notFoundErr, 193 | false, 194 | nil, 195 | &scalewaymetav1alpha1.Status{ 196 | Conditions: []scalewaymetav1alpha1.Condition{ 197 | { 198 | Type: scalewaymetav1alpha1.Reconciled, 199 | LastProbeTime: now, 200 | LastTransitionTime: before, 201 | Message: notFoundErr.Error(), 202 | Reason: reasonResourceNotFound, 203 | Status: corev1.ConditionFalse, 204 | }, 205 | }, 206 | }, 207 | }, 208 | { 209 | &scalewaymetav1alpha1.Status{ 210 | Conditions: []scalewaymetav1alpha1.Condition{ 211 | { 212 | Type: scalewaymetav1alpha1.Reconciled, 213 | LastProbeTime: before, 214 | LastTransitionTime: before, 215 | Message: messageStillReconciling, 216 | Reason: reasonReconciling, 217 | Status: corev1.ConditionFalse, 218 | }, 219 | }, 220 | }, 221 | tStateErr, 222 | false, 223 | nil, 224 | &scalewaymetav1alpha1.Status{ 225 | Conditions: []scalewaymetav1alpha1.Condition{ 226 | { 227 | Type: scalewaymetav1alpha1.Reconciled, 228 | LastProbeTime: now, 229 | LastTransitionTime: before, 230 | Message: tStateErr.Error(), 231 | Reason: reasonTransientState, 232 | Status: corev1.ConditionFalse, 233 | }, 234 | }, 235 | }, 236 | }, 237 | { 238 | &scalewaymetav1alpha1.Status{ 239 | Conditions: []scalewaymetav1alpha1.Condition{ 240 | { 241 | Type: scalewaymetav1alpha1.Reconciled, 242 | LastProbeTime: before, 243 | LastTransitionTime: before, 244 | Message: messageStillReconciling, 245 | Reason: reasonReconciling, 246 | Status: corev1.ConditionFalse, 247 | }, 248 | }, 249 | }, 250 | permDeniedErr, 251 | false, 252 | nil, 253 | &scalewaymetav1alpha1.Status{ 254 | Conditions: []scalewaymetav1alpha1.Condition{ 255 | { 256 | Type: scalewaymetav1alpha1.Reconciled, 257 | LastProbeTime: now, 258 | LastTransitionTime: before, 259 | Message: permDeniedErr.Error(), 260 | Reason: reasonPermissionsDenied, 261 | Status: corev1.ConditionFalse, 262 | }, 263 | }, 264 | }, 265 | }, 266 | { 267 | &scalewaymetav1alpha1.Status{ 268 | Conditions: []scalewaymetav1alpha1.Condition{ 269 | { 270 | Type: scalewaymetav1alpha1.Reconciled, 271 | LastProbeTime: before, 272 | LastTransitionTime: before, 273 | Message: messageStillReconciling, 274 | Reason: reasonReconciling, 275 | Status: corev1.ConditionFalse, 276 | }, 277 | }, 278 | }, 279 | oosErr, 280 | false, 281 | nil, 282 | &scalewaymetav1alpha1.Status{ 283 | Conditions: []scalewaymetav1alpha1.Condition{ 284 | { 285 | Type: scalewaymetav1alpha1.Reconciled, 286 | LastProbeTime: now, 287 | LastTransitionTime: before, 288 | Message: oosErr.Error(), 289 | Reason: reasonOutOfStock, 290 | Status: corev1.ConditionFalse, 291 | }, 292 | }, 293 | }, 294 | }, 295 | { 296 | &scalewaymetav1alpha1.Status{ 297 | Conditions: []scalewaymetav1alpha1.Condition{ 298 | { 299 | Type: scalewaymetav1alpha1.Reconciled, 300 | LastProbeTime: before, 301 | LastTransitionTime: before, 302 | Message: messageStillReconciling, 303 | Reason: reasonReconciling, 304 | Status: corev1.ConditionFalse, 305 | }, 306 | }, 307 | }, 308 | quotasExceededError, 309 | false, 310 | nil, 311 | &scalewaymetav1alpha1.Status{ 312 | Conditions: []scalewaymetav1alpha1.Condition{ 313 | { 314 | Type: scalewaymetav1alpha1.Reconciled, 315 | LastProbeTime: now, 316 | LastTransitionTime: before, 317 | Message: quotasExceededError.Error(), 318 | Reason: reasonQuotasExceeded, 319 | Status: corev1.ConditionFalse, 320 | }, 321 | }, 322 | }, 323 | }, 324 | { 325 | &scalewaymetav1alpha1.Status{ 326 | Conditions: []scalewaymetav1alpha1.Condition{ 327 | { 328 | Type: scalewaymetav1alpha1.Reconciled, 329 | LastProbeTime: before, 330 | LastTransitionTime: before, 331 | Message: messageStillReconciling, 332 | Reason: reasonReconciling, 333 | Status: corev1.ConditionFalse, 334 | }, 335 | }, 336 | }, 337 | resLockedErr, 338 | false, 339 | nil, 340 | &scalewaymetav1alpha1.Status{ 341 | Conditions: []scalewaymetav1alpha1.Condition{ 342 | { 343 | Type: scalewaymetav1alpha1.Reconciled, 344 | LastProbeTime: now, 345 | LastTransitionTime: before, 346 | Message: resLockedErr.Error(), 347 | Reason: reasonResourceLocked, 348 | Status: corev1.ConditionFalse, 349 | }, 350 | }, 351 | }, 352 | }, 353 | { 354 | &scalewaymetav1alpha1.Status{ 355 | Conditions: []scalewaymetav1alpha1.Condition{ 356 | { 357 | Type: scalewaymetav1alpha1.Reconciled, 358 | LastProbeTime: before, 359 | LastTransitionTime: before, 360 | Message: messageStillReconciling, 361 | Reason: reasonReconciling, 362 | Status: corev1.ConditionFalse, 363 | }, 364 | }, 365 | }, 366 | invalidArgErr, 367 | false, 368 | nil, 369 | &scalewaymetav1alpha1.Status{ 370 | Conditions: []scalewaymetav1alpha1.Condition{ 371 | { 372 | Type: scalewaymetav1alpha1.Reconciled, 373 | LastProbeTime: now, 374 | LastTransitionTime: before, 375 | Message: invalidArgErr.Error(), 376 | Reason: reasonInvalidArguments, 377 | Status: corev1.ConditionFalse, 378 | }, 379 | }, 380 | }, 381 | }, 382 | } 383 | for _, c := range cases { 384 | _, err := updateStatus(c.status, now, c.ensureErr, c.reconciled) 385 | if err != c.err { 386 | t.Errorf("Got error %+v instead of %+v", err, c.err) 387 | } 388 | if !compareStatus(c.newStatus, c.status) { 389 | t.Errorf("Got status %+v instead of %+v", *c.status, *c.newStatus) 390 | } 391 | } 392 | } 393 | 394 | func Test_updateCondition(t *testing.T) { 395 | now := metav1.NewTime(time.Now()) 396 | before := metav1.NewTime(time.Now().Add(-5 * time.Second)) 397 | 398 | cases := []struct { 399 | status *scalewaymetav1alpha1.Status 400 | condition scalewaymetav1alpha1.Condition 401 | newStatus *scalewaymetav1alpha1.Status 402 | }{ 403 | { 404 | &scalewaymetav1alpha1.Status{ 405 | Conditions: []scalewaymetav1alpha1.Condition{}, 406 | }, 407 | scalewaymetav1alpha1.Condition{ 408 | Type: scalewaymetav1alpha1.Ready, 409 | Message: "message", 410 | Reason: "reason", 411 | Status: corev1.ConditionTrue, 412 | }, 413 | &scalewaymetav1alpha1.Status{ 414 | Conditions: []scalewaymetav1alpha1.Condition{ 415 | { 416 | Type: scalewaymetav1alpha1.Ready, 417 | LastProbeTime: now, 418 | LastTransitionTime: now, 419 | Message: "message", 420 | Reason: "reason", 421 | Status: corev1.ConditionTrue, 422 | }, 423 | }, 424 | }, 425 | }, 426 | { 427 | &scalewaymetav1alpha1.Status{ 428 | Conditions: []scalewaymetav1alpha1.Condition{ 429 | { 430 | Type: scalewaymetav1alpha1.Ready, 431 | LastProbeTime: before, 432 | LastTransitionTime: before, 433 | Message: "message", 434 | Reason: "reason", 435 | Status: corev1.ConditionTrue, 436 | }, 437 | }, 438 | }, 439 | scalewaymetav1alpha1.Condition{ 440 | Type: scalewaymetav1alpha1.Ready, 441 | Message: "message", 442 | Reason: "reason", 443 | Status: corev1.ConditionTrue, 444 | }, 445 | &scalewaymetav1alpha1.Status{ 446 | Conditions: []scalewaymetav1alpha1.Condition{ 447 | { 448 | Type: scalewaymetav1alpha1.Ready, 449 | LastProbeTime: now, 450 | LastTransitionTime: before, 451 | Message: "message", 452 | Reason: "reason", 453 | Status: corev1.ConditionTrue, 454 | }, 455 | }, 456 | }, 457 | }, 458 | { 459 | &scalewaymetav1alpha1.Status{ 460 | Conditions: []scalewaymetav1alpha1.Condition{ 461 | { 462 | Type: scalewaymetav1alpha1.Ready, 463 | LastProbeTime: before, 464 | LastTransitionTime: before, 465 | Message: "message", 466 | Reason: "reason", 467 | Status: corev1.ConditionTrue, 468 | }, 469 | }, 470 | }, 471 | scalewaymetav1alpha1.Condition{ 472 | Type: scalewaymetav1alpha1.Ready, 473 | Message: "message", 474 | Reason: "reason", 475 | Status: corev1.ConditionFalse, 476 | }, 477 | &scalewaymetav1alpha1.Status{ 478 | Conditions: []scalewaymetav1alpha1.Condition{ 479 | { 480 | Type: scalewaymetav1alpha1.Ready, 481 | LastProbeTime: now, 482 | LastTransitionTime: now, 483 | Message: "message", 484 | Reason: "reason", 485 | Status: corev1.ConditionFalse, 486 | }, 487 | }, 488 | }, 489 | }, 490 | } 491 | 492 | for _, c := range cases { 493 | updateCondition(c.status, c.condition, now) 494 | if !compareStatus(c.newStatus, c.status) { 495 | t.Errorf("Got status %+v instead of %+v", *c.status, *c.newStatus) 496 | } 497 | } 498 | } 499 | -------------------------------------------------------------------------------- /deploy/scaleway-operator-secrets.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | stringData: 3 | SCW_ACCESS_KEY: 4 | SCW_SECRET_KEY: 5 | SCW_DEFAULT_PROJECT_ID: 6 | SCW_DEFAULT_REGION: 7 | kind: Secret 8 | metadata: 9 | name: scaleway-operator-secrets 10 | namespace: scaleway-operator-system 11 | type: Opaque 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/scaleway/scaleway-operator 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/dnaeon/go-vcr v1.0.1 7 | github.com/go-logr/logr v0.1.0 8 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7 9 | k8s.io/api v0.18.6 10 | k8s.io/apimachinery v0.18.6 11 | k8s.io/client-go v0.18.6 12 | sigs.k8s.io/controller-runtime v0.6.1 13 | ) 14 | -------------------------------------------------------------------------------- /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 | */ -------------------------------------------------------------------------------- /internal/testhelpers/httprecorder/recorder.go: -------------------------------------------------------------------------------- 1 | package httprecorder 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "strings" 9 | 10 | "github.com/dnaeon/go-vcr/cassette" 11 | "github.com/dnaeon/go-vcr/recorder" 12 | "github.com/scaleway/scaleway-sdk-go/scw" 13 | ) 14 | 15 | const ( 16 | envUpdateTestdata = "UPDATE_TESTDATA" 17 | envDisableTestdata = "DISABLE_TESTDATA" 18 | ) 19 | 20 | func UpdateTestdata() bool { 21 | return os.Getenv(envUpdateTestdata) == "true" 22 | } 23 | 24 | func DisableTestData() bool { 25 | return os.Getenv(envDisableTestdata) == "true" 26 | } 27 | 28 | func CreateRecordedScwClient(cassetteName string) (*scw.Client, *recorder.Recorder, error) { 29 | recorderMode := recorder.ModeReplaying 30 | if UpdateTestdata() { 31 | recorderMode = recorder.ModeRecording 32 | } 33 | if DisableTestData() { 34 | recorderMode = recorder.ModeDisabled 35 | } 36 | 37 | r, err := recorder.NewAsMode(fmt.Sprintf("testdata/%s", cassetteName), recorderMode, nil) 38 | if err != nil { 39 | return nil, nil, err 40 | } 41 | 42 | // Do not record secret keys 43 | r.AddFilter(func(i *cassette.Interaction) error { 44 | delete(i.Request.Headers, "x-auth-token") 45 | delete(i.Request.Headers, "X-Auth-Token") 46 | 47 | // panics if the secret key is found elsewhere 48 | if UpdateTestdata() && !DisableTestData() { 49 | if i != nil && strings.Contains(fmt.Sprintf("%v", *i), os.Getenv(scw.ScwSecretKeyEnv)) { 50 | panic(errors.New("found secret key in cassette")) 51 | } 52 | } 53 | 54 | return nil 55 | }) 56 | 57 | // Create new http.Client where transport is the recorder 58 | httpClient := &http.Client{Transport: r} 59 | 60 | var client *scw.Client 61 | 62 | if UpdateTestdata() || DisableTestData() { 63 | // When updating the recoreded test requests, we need the access key and secret key. 64 | client, err = scw.NewClient( 65 | scw.WithHTTPClient(httpClient), 66 | scw.WithEnv(), 67 | scw.WithDefaultRegion(scw.RegionFrPar), 68 | scw.WithDefaultZone(scw.ZoneFrPar1), 69 | ) 70 | if err != nil { 71 | return nil, nil, err 72 | } 73 | } else { 74 | // No need for auth when using cassette 75 | client, err = scw.NewClient( 76 | scw.WithHTTPClient(httpClient), 77 | scw.WithDefaultRegion(scw.RegionFrPar), 78 | scw.WithDefaultZone(scw.ZoneFrPar1), 79 | ) 80 | if err != nil { 81 | return nil, nil, err 82 | } 83 | } 84 | return client, r, nil 85 | } 86 | -------------------------------------------------------------------------------- /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 | 23 | "k8s.io/apimachinery/pkg/runtime" 24 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 25 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 26 | ctrl "sigs.k8s.io/controller-runtime" 27 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 28 | 29 | "github.com/scaleway/scaleway-sdk-go/api/rdb/v1" 30 | "github.com/scaleway/scaleway-sdk-go/scw" 31 | 32 | rdbv1alpha1 "github.com/scaleway/scaleway-operator/apis/rdb/v1alpha1" 33 | "github.com/scaleway/scaleway-operator/controllers" 34 | rdbcontroller "github.com/scaleway/scaleway-operator/controllers/rdb" 35 | rdbmanager "github.com/scaleway/scaleway-operator/pkg/manager/rdb" 36 | "github.com/scaleway/scaleway-operator/webhooks" 37 | rdbwebhook "github.com/scaleway/scaleway-operator/webhooks/rdb" 38 | // +kubebuilder:scaffold:imports 39 | ) 40 | 41 | var ( 42 | scheme = runtime.NewScheme() 43 | setupLog = ctrl.Log.WithName("setup") 44 | ) 45 | 46 | func init() { 47 | _ = clientgoscheme.AddToScheme(scheme) 48 | 49 | _ = rdbv1alpha1.AddToScheme(scheme) 50 | // +kubebuilder:scaffold:scheme 51 | } 52 | 53 | func main() { 54 | var metricsAddr string 55 | var enableLeaderElection bool 56 | flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds 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.UseDevMode(true))) 63 | 64 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 65 | Scheme: scheme, 66 | MetricsBindAddress: metricsAddr, 67 | Port: 9443, 68 | LeaderElection: enableLeaderElection, 69 | LeaderElectionID: "e0607a04.scaleway.com", 70 | }) 71 | if err != nil { 72 | setupLog.Error(err, "unable to start manager") 73 | os.Exit(1) 74 | } 75 | 76 | scwClient, err := scw.NewClient(scw.WithEnv()) 77 | if err != nil { 78 | setupLog.Error(err, "unable to create scw client") 79 | os.Exit(1) 80 | } 81 | 82 | if err = (&rdbcontroller.RDBInstanceReconciler{ 83 | ScalewayReconciler: &controllers.ScalewayReconciler{ 84 | Client: mgr.GetClient(), 85 | Log: ctrl.Log.WithName("controllers").WithName("RDBInstance"), 86 | Recorder: mgr.GetEventRecorderFor("RDBInstance"), 87 | Scheme: mgr.GetScheme(), 88 | ScalewayManager: &rdbmanager.InstanceManager{ 89 | API: rdb.NewAPI(scwClient), 90 | Client: mgr.GetClient(), 91 | Log: ctrl.Log.WithName("manager").WithName("RDBInstance"), 92 | }, 93 | }, 94 | }).SetupWithManager(mgr); err != nil { 95 | setupLog.Error(err, "unable to create controller", "controller", "RDBInstance") 96 | os.Exit(1) 97 | } 98 | 99 | if err = (&rdbcontroller.RDBDatabaseReconciler{ 100 | ScalewayReconciler: &controllers.ScalewayReconciler{ 101 | Client: mgr.GetClient(), 102 | Log: ctrl.Log.WithName("controllers").WithName("RDBDatabase"), 103 | Recorder: mgr.GetEventRecorderFor("RDBDatabase"), 104 | Scheme: mgr.GetScheme(), 105 | ScalewayManager: &rdbmanager.DatabaseManager{ 106 | API: rdb.NewAPI(scwClient), 107 | Client: mgr.GetClient(), 108 | }, 109 | }, 110 | }).SetupWithManager(mgr); err != nil { 111 | setupLog.Error(err, "unable to create controller", "controller", "RDBDatabase") 112 | os.Exit(1) 113 | } 114 | 115 | if err = (&rdbcontroller.RDBUserReconciler{ 116 | ScalewayReconciler: &controllers.ScalewayReconciler{ 117 | Client: mgr.GetClient(), 118 | Log: ctrl.Log.WithName("controllers").WithName("RDBUser"), 119 | Recorder: mgr.GetEventRecorderFor("RDBDatabase"), 120 | Scheme: mgr.GetScheme(), 121 | ScalewayManager: &rdbmanager.DatabaseManager{ 122 | API: rdb.NewAPI(scwClient), 123 | Client: mgr.GetClient(), 124 | }, 125 | }, 126 | }).SetupWithManager(mgr); err != nil { 127 | setupLog.Error(err, "unable to create controller", "controller", "RDBUser") 128 | os.Exit(1) 129 | } 130 | // +kubebuilder:scaffold:builder 131 | 132 | if os.Getenv("ENABLE_WEBHOOKS") != "false" { 133 | if err = (&rdbwebhook.RDBInstanceValidator{ 134 | Log: ctrl.Log.WithName("webhooks").WithName("RDBInstance"), 135 | ScalewayWebhook: &webhooks.ScalewayWebhook{ 136 | ScalewayManager: &rdbmanager.InstanceManager{ 137 | API: rdb.NewAPI(scwClient), 138 | Client: mgr.GetClient(), 139 | }, 140 | }, 141 | }).SetupWebhookWithManager(mgr); err != nil { 142 | setupLog.Error(err, "unable to create webhook", "webhook", "RDBInstance") 143 | os.Exit(1) 144 | } 145 | 146 | if err = (&rdbwebhook.RDBDatabaseValidator{ 147 | Log: ctrl.Log.WithName("webhooks").WithName("RDBDatabase"), 148 | ScalewayWebhook: &webhooks.ScalewayWebhook{ 149 | ScalewayManager: &rdbmanager.DatabaseManager{ 150 | API: rdb.NewAPI(scwClient), 151 | Client: mgr.GetClient(), 152 | }, 153 | }, 154 | }).SetupWebhookWithManager(mgr); err != nil { 155 | setupLog.Error(err, "unable to create webhook", "webhook", "RDBDatabase") 156 | os.Exit(1) 157 | } 158 | } 159 | 160 | setupLog.Info("starting manager") 161 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 162 | setupLog.Error(err, "problem running manager") 163 | os.Exit(1) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /pkg/manager/rdb/database.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/scaleway/scaleway-operator/pkg/manager/scaleway" 8 | "github.com/scaleway/scaleway-sdk-go/api/rdb/v1" 9 | "github.com/scaleway/scaleway-sdk-go/scw" 10 | "k8s.io/apimachinery/pkg/api/resource" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | "k8s.io/apimachinery/pkg/types" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | 15 | rdbv1alpha1 "github.com/scaleway/scaleway-operator/apis/rdb/v1alpha1" 16 | ) 17 | 18 | // DatabaseManager manages the RDB databsses 19 | type DatabaseManager struct { 20 | client.Client 21 | API *rdb.API 22 | scaleway.Manager 23 | } 24 | 25 | // Ensure reconciles the RDB database resource 26 | func (m *DatabaseManager) Ensure(ctx context.Context, obj runtime.Object) (bool, error) { 27 | database, err := convertDatabase(obj) 28 | if err != nil { 29 | return false, err 30 | } 31 | 32 | instanceID, region, err := m.getInstanceIDAndRegion(ctx, database) 33 | if err != nil { 34 | return false, err 35 | } 36 | 37 | rdbDatabase, err := m.getByName(ctx, database) 38 | if err != nil { 39 | return false, err 40 | } 41 | 42 | if rdbDatabase != nil { 43 | // pass for now 44 | } else { 45 | name := database.GetName() 46 | if database.Spec.OverrideName != "" { 47 | name = database.Spec.OverrideName 48 | } 49 | rdbDatabase, err = m.API.CreateDatabase(&rdb.CreateDatabaseRequest{ 50 | Region: region, 51 | InstanceID: instanceID, 52 | Name: name, 53 | }) 54 | if err != nil { 55 | return false, err 56 | } 57 | } 58 | 59 | database.Status.Managed = rdbDatabase.Managed 60 | database.Status.Owner = rdbDatabase.Owner 61 | database.Status.Size = resource.NewQuantity(int64(rdbDatabase.Size), resource.DecimalSI) 62 | 63 | return true, nil 64 | } 65 | 66 | // Delete deletes the RDB database resource 67 | func (m *DatabaseManager) Delete(ctx context.Context, obj runtime.Object) (bool, error) { 68 | database, err := convertDatabase(obj) 69 | if err != nil { 70 | return false, err 71 | } 72 | 73 | instanceID, region, err := m.getInstanceIDAndRegion(ctx, database) 74 | if err != nil { 75 | return false, err 76 | } 77 | 78 | name := database.GetName() 79 | if database.Spec.OverrideName != "" { 80 | name = database.Spec.OverrideName 81 | } 82 | 83 | err = m.API.DeleteDatabase(&rdb.DeleteDatabaseRequest{ 84 | Region: region, 85 | InstanceID: instanceID, 86 | Name: name, 87 | }) 88 | if err != nil { 89 | if _, ok := err.(*scw.ResourceNotFoundError); ok { 90 | return true, nil 91 | } 92 | return false, err 93 | } 94 | 95 | return true, nil 96 | } 97 | 98 | func (m *DatabaseManager) getByName(ctx context.Context, database *rdbv1alpha1.RDBDatabase) (*rdb.Database, error) { 99 | instanceID, region, err := m.getInstanceIDAndRegion(ctx, database) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | name := database.GetName() 105 | if database.Spec.OverrideName != "" { 106 | name = database.Spec.OverrideName 107 | } 108 | 109 | databasesResp, err := m.API.ListDatabases(&rdb.ListDatabasesRequest{ 110 | Region: region, 111 | InstanceID: instanceID, 112 | Name: scw.StringPtr(name), 113 | }, scw.WithAllPages()) 114 | if err != nil { 115 | if _, ok := err.(*scw.ResourceNotFoundError); ok { 116 | return nil, nil 117 | } 118 | return nil, err 119 | } 120 | 121 | var finalDatabase *rdb.Database 122 | 123 | for _, rdbDatabase := range databasesResp.Databases { 124 | if rdbDatabase.Name == name { 125 | finalDatabase = rdbDatabase 126 | break 127 | } 128 | } 129 | 130 | if finalDatabase != nil { 131 | return finalDatabase, nil 132 | } 133 | 134 | return nil, nil 135 | } 136 | 137 | // GetOwners returns the owners of the RDB database resource 138 | func (m *DatabaseManager) GetOwners(ctx context.Context, obj runtime.Object) ([]scaleway.Owner, error) { 139 | database, err := convertDatabase(obj) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | if database.Spec.InstanceRef.Name == "" { 145 | return nil, nil 146 | } 147 | 148 | instance := &rdbv1alpha1.RDBInstance{} 149 | 150 | databaseNamespace := database.Spec.InstanceRef.Namespace 151 | if databaseNamespace == "" { 152 | databaseNamespace = database.Namespace 153 | } 154 | 155 | err = m.Get(ctx, client.ObjectKey{Name: database.Spec.InstanceRef.Name, Namespace: databaseNamespace}, instance) 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | return []scaleway.Owner{ 161 | { 162 | Key: types.NamespacedName{ 163 | Name: instance.Name, 164 | Namespace: instance.Namespace, 165 | }, 166 | Object: &rdbv1alpha1.RDBInstance{}, 167 | }, 168 | }, nil 169 | } 170 | 171 | func (m *DatabaseManager) getInstanceIDAndRegion(ctx context.Context, database *rdbv1alpha1.RDBDatabase) (string, scw.Region, error) { 172 | instance := &rdbv1alpha1.RDBInstance{} 173 | 174 | if database.Spec.InstanceRef.Name != "" { 175 | databaseNamespace := database.Spec.InstanceRef.Namespace 176 | if databaseNamespace == "" { 177 | databaseNamespace = database.Namespace 178 | } 179 | 180 | err := m.Get(ctx, client.ObjectKey{Name: database.Spec.InstanceRef.Name, Namespace: databaseNamespace}, instance) 181 | if err != nil { 182 | return "", "", err 183 | } 184 | 185 | return instance.Spec.InstanceID, scw.Region(instance.Spec.Region), nil 186 | } 187 | 188 | return database.Spec.InstanceRef.ExternalID, scw.Region(database.Spec.InstanceRef.Region), nil 189 | } 190 | 191 | func convertDatabase(obj runtime.Object) (*rdbv1alpha1.RDBDatabase, error) { 192 | database, ok := obj.(*rdbv1alpha1.RDBDatabase) 193 | if !ok { 194 | return nil, fmt.Errorf("failed type assertion on kind: %s", obj.GetObjectKind().GroupVersionKind().String()) 195 | } 196 | return database, nil 197 | } 198 | -------------------------------------------------------------------------------- /pkg/manager/rdb/database_webhook.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/scaleway/scaleway-sdk-go/api/rdb/v1" 7 | "github.com/scaleway/scaleway-sdk-go/scw" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/util/validation/field" 10 | ) 11 | 12 | // ValidateCreate validates the creation of a RDB Database 13 | func (m *DatabaseManager) ValidateCreate(ctx context.Context, obj runtime.Object) (field.ErrorList, error) { 14 | var allErrs field.ErrorList 15 | 16 | database, err := convertDatabase(obj) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | byName := database.Spec.InstanceRef.Name != "" || database.Spec.InstanceRef.Namespace != "" 22 | byID := database.Spec.InstanceRef.ExternalID != "" || database.Spec.InstanceRef.Region != "" 23 | 24 | if database.Spec.InstanceRef.Name == "" && database.Spec.InstanceRef.ExternalID == "" { 25 | allErrs = append(allErrs, field.Required(field.NewPath("spec").Child("instanceRef"), "name/namespace or externalID/region must be specified")) 26 | return allErrs, nil 27 | } 28 | if byName && byID { 29 | allErrs = append(allErrs, field.Forbidden(field.NewPath("spec").Child("instanceRef"), "only on of name/namespace and externalID/region must be specified")) 30 | return allErrs, nil 31 | } 32 | 33 | if byID { 34 | _, err = scw.ParseRegion(database.Spec.InstanceRef.Region) 35 | if database.Spec.InstanceRef.Region != "" && err != nil { 36 | allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("instanceRef").Child("region"), database.Spec.InstanceRef.Region, "region is not valid")) 37 | return allErrs, nil 38 | } 39 | 40 | _, err = m.API.GetInstance(&rdb.GetInstanceRequest{ 41 | Region: scw.Region(database.Spec.InstanceRef.Region), 42 | InstanceID: database.Spec.InstanceRef.ExternalID, 43 | }) 44 | if err != nil { 45 | allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("instanceRef").Child("externalID"), database.Spec.InstanceRef.ExternalID, err.Error())) 46 | return allErrs, nil 47 | } 48 | } 49 | 50 | return allErrs, nil 51 | } 52 | 53 | // ValidateUpdate validates the update of a RDB Database 54 | func (m *DatabaseManager) ValidateUpdate(ctx context.Context, oldObj runtime.Object, obj runtime.Object) (field.ErrorList, error) { 55 | var allErrs field.ErrorList 56 | 57 | database, err := convertDatabase(obj) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | oldDatabase, err := convertDatabase(oldObj) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | if oldDatabase.Spec.OverrideName != oldDatabase.Spec.OverrideName { 68 | allErrs = append(allErrs, field.Forbidden(field.NewPath("spec").Child("overrideName"), "field is immutable")) 69 | } 70 | 71 | if oldDatabase.Spec.InstanceRef.Region != database.Spec.InstanceRef.Region { 72 | allErrs = append(allErrs, field.Forbidden(field.NewPath("spec").Child("instanceRef").Child("region"), "field is immutable")) 73 | } 74 | 75 | if oldDatabase.Spec.InstanceRef.ExternalID != database.Spec.InstanceRef.ExternalID { 76 | allErrs = append(allErrs, field.Forbidden(field.NewPath("spec").Child("instanceRef").Child("externalID"), "field is immutable")) 77 | } 78 | 79 | if oldDatabase.Spec.InstanceRef.Name != database.Spec.InstanceRef.Name { 80 | allErrs = append(allErrs, field.Forbidden(field.NewPath("spec").Child("instanceRef").Child("name"), "field is immutable")) 81 | } 82 | 83 | if oldDatabase.Spec.InstanceRef.Namespace != database.Spec.InstanceRef.Namespace { 84 | allErrs = append(allErrs, field.Forbidden(field.NewPath("spec").Child("instanceRef").Child("namespace"), "field is immutable")) 85 | } 86 | 87 | return allErrs, nil 88 | } 89 | -------------------------------------------------------------------------------- /pkg/manager/rdb/instance.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | 8 | "github.com/go-logr/logr" 9 | "github.com/scaleway/scaleway-operator/pkg/manager/scaleway" 10 | "github.com/scaleway/scaleway-operator/pkg/utils" 11 | "github.com/scaleway/scaleway-sdk-go/api/rdb/v1" 12 | "github.com/scaleway/scaleway-sdk-go/scw" 13 | corev1 "k8s.io/api/core/v1" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | 17 | rdbv1alpha1 "github.com/scaleway/scaleway-operator/apis/rdb/v1alpha1" 18 | ) 19 | 20 | // InstanceManager manages the RDB instances 21 | type InstanceManager struct { 22 | client.Client 23 | API *rdb.API 24 | scaleway.Manager 25 | Log logr.Logger 26 | } 27 | 28 | // Ensure reconciles the RDB instance resource 29 | func (m *InstanceManager) Ensure(ctx context.Context, obj runtime.Object) (bool, error) { 30 | instance, err := convertInstance(obj) 31 | if err != nil { 32 | return false, err 33 | } 34 | 35 | region := scw.Region(instance.Spec.Region) 36 | 37 | // if instanceID is empty, we need to create the instance 38 | if instance.Spec.InstanceID == "" { 39 | return false, m.createInstance(ctx, instance) 40 | } 41 | 42 | rdbInstanceResp, err := m.API.GetInstance(&rdb.GetInstanceRequest{ 43 | Region: region, 44 | InstanceID: instance.Spec.InstanceID, 45 | }) 46 | if err != nil { 47 | return false, err 48 | } 49 | 50 | needReturn, err := m.updateInstance(instance, rdbInstanceResp) 51 | if err != nil { 52 | return false, err 53 | } 54 | if needReturn { 55 | return false, nil 56 | } 57 | 58 | needReturn, err = m.upgradeInstance(instance, rdbInstanceResp) 59 | if err != nil { 60 | return false, err 61 | } 62 | if needReturn { 63 | return false, nil 64 | } 65 | 66 | if instance.Spec.ACL != nil { 67 | err = m.updateACLs(ctx, instance, rdbInstanceResp) 68 | if err != nil { 69 | return false, err 70 | } 71 | } 72 | 73 | if rdbInstanceResp.Endpoint != nil { 74 | instance.Status.Endpoint.IP = rdbInstanceResp.Endpoint.IP.String() 75 | instance.Status.Endpoint.Port = int32(rdbInstanceResp.Endpoint.Port) 76 | } 77 | 78 | return rdbInstanceResp.Status == rdb.InstanceStatusReady, nil 79 | } 80 | 81 | // Delete deletes the RDB instance resource 82 | func (m *InstanceManager) Delete(ctx context.Context, obj runtime.Object) (bool, error) { 83 | instance, err := convertInstance(obj) 84 | if err != nil { 85 | return false, err 86 | } 87 | 88 | region := scw.Region(instance.Spec.Region) 89 | 90 | resourceID := instance.Spec.InstanceID 91 | if resourceID == "" { 92 | return true, nil 93 | } 94 | 95 | _, err = m.API.DeleteInstance(&rdb.DeleteInstanceRequest{ 96 | Region: region, 97 | InstanceID: resourceID, 98 | }) 99 | if err != nil { 100 | if _, ok := err.(*scw.ResourceNotFoundError); ok { 101 | return true, nil 102 | } 103 | return false, err 104 | } 105 | 106 | //instance.Status.Status = strcase.ToCamel(instanceResp.Status.String()) 107 | 108 | return false, nil 109 | } 110 | 111 | // GetOwners returns the owners of the RDB instance resource 112 | func (m *InstanceManager) GetOwners(ctx context.Context, obj runtime.Object) ([]scaleway.Owner, error) { 113 | return nil, nil 114 | } 115 | 116 | func (m *InstanceManager) createInstance(ctx context.Context, instance *rdbv1alpha1.RDBInstance) error { 117 | region := scw.Region(instance.Spec.Region) 118 | 119 | if instance.Spec.InstanceFrom != nil { 120 | instanceID, region, err := m.getInstanceFromIDAndRegion(ctx, instance) 121 | if err != nil { 122 | return err 123 | } 124 | rdbInstanceResp, err := m.API.CloneInstance(&rdb.CloneInstanceRequest{ 125 | Region: region, 126 | InstanceID: instanceID, 127 | NodeType: scw.StringPtr(instance.Spec.NodeType), 128 | Name: instance.Name, 129 | }) 130 | if err != nil { 131 | return err 132 | } 133 | instance.Spec.InstanceID = rdbInstanceResp.ID 134 | instance.Spec.Region = rdbInstanceResp.Region.String() 135 | err = m.Client.Update(ctx, instance) 136 | if err != nil { 137 | return err 138 | } 139 | 140 | return nil 141 | } 142 | 143 | disableBackup := true 144 | if instance.Spec.AutoBackup != nil { 145 | disableBackup = instance.Spec.AutoBackup.Disabled 146 | } 147 | 148 | createRequest := &rdb.CreateInstanceRequest{ 149 | Region: region, 150 | DisableBackup: disableBackup, 151 | Engine: instance.Spec.Engine, 152 | IsHaCluster: instance.Spec.IsHaCluster, 153 | Name: instance.Name, 154 | NodeType: instance.Spec.NodeType, 155 | Tags: utils.LabelsToTags(instance.Labels), 156 | } 157 | 158 | rdbInstanceResp, err := m.API.CreateInstance(createRequest) 159 | if err != nil { 160 | return err 161 | } 162 | instance.Spec.InstanceID = rdbInstanceResp.ID 163 | instance.Spec.Region = rdbInstanceResp.Region.String() 164 | err = m.Client.Update(ctx, instance) 165 | if err != nil { 166 | return err 167 | } 168 | 169 | return nil 170 | } 171 | 172 | func (m *InstanceManager) updateInstance(instance *rdbv1alpha1.RDBInstance, rdbInstance *rdb.Instance) (bool, error) { 173 | needsUpdate := false 174 | updateRequest := &rdb.UpdateInstanceRequest{ 175 | Region: scw.Region(instance.Spec.Region), 176 | InstanceID: instance.Spec.InstanceID, 177 | } 178 | 179 | if !utils.CompareTagsLabels(rdbInstance.Tags, instance.Labels) { 180 | updateRequest.Tags = scw.StringsPtr(utils.LabelsToTags(instance.Labels)) 181 | needsUpdate = true 182 | } 183 | 184 | if instance.Spec.AutoBackup != nil && rdbInstance.BackupSchedule != nil { 185 | if instance.Spec.AutoBackup.Disabled != rdbInstance.BackupSchedule.Disabled { 186 | updateRequest.IsBackupScheduleDisabled = scw.BoolPtr(instance.Spec.AutoBackup.Disabled) 187 | needsUpdate = true 188 | } 189 | if instance.Spec.AutoBackup.Frequency != nil && uint32(*instance.Spec.AutoBackup.Frequency) != rdbInstance.BackupSchedule.Frequency { 190 | updateRequest.BackupScheduleFrequency = scw.Uint32Ptr(uint32(*instance.Spec.AutoBackup.Frequency)) 191 | needsUpdate = true 192 | } 193 | if instance.Spec.AutoBackup.Retention != nil && uint32(*instance.Spec.AutoBackup.Retention) != rdbInstance.BackupSchedule.Retention { 194 | updateRequest.BackupScheduleRetention = scw.Uint32Ptr(uint32(*instance.Spec.AutoBackup.Retention)) 195 | needsUpdate = true 196 | } 197 | } 198 | 199 | if needsUpdate { 200 | _, err := m.API.UpdateInstance(updateRequest) 201 | if err != nil { 202 | return false, err 203 | } 204 | return true, nil 205 | } 206 | 207 | return false, nil 208 | } 209 | 210 | func (m *InstanceManager) upgradeInstance(instance *rdbv1alpha1.RDBInstance, rdbInstance *rdb.Instance) (bool, error) { 211 | upgradeRequest := &rdb.UpgradeInstanceRequest{ 212 | Region: scw.Region(instance.Spec.Region), 213 | InstanceID: instance.Spec.InstanceID, 214 | } 215 | 216 | if rdbInstance.IsHaCluster != instance.Spec.IsHaCluster { 217 | upgradeRequest.EnableHa = scw.BoolPtr(instance.Spec.IsHaCluster) 218 | _, err := m.API.UpgradeInstance(upgradeRequest) 219 | if err != nil { 220 | return false, err 221 | } 222 | return true, nil 223 | } 224 | 225 | if rdbInstance.NodeType != instance.Spec.NodeType { 226 | upgradeRequest.NodeType = scw.StringPtr(instance.Spec.NodeType) 227 | _, err := m.API.UpgradeInstance(upgradeRequest) 228 | if err != nil { 229 | return false, err 230 | } 231 | return true, nil 232 | } 233 | 234 | return false, nil 235 | } 236 | 237 | func (m *InstanceManager) getNodesIP(ctx context.Context) ([]net.IPNet, error) { 238 | var nodesIP []net.IPNet 239 | 240 | nodesList := corev1.NodeList{} 241 | err := m.Client.List(ctx, &nodesList) 242 | if err != nil { 243 | return nil, err 244 | } 245 | for _, node := range nodesList.Items { 246 | for _, addr := range node.Status.Addresses { 247 | if addr.Type == corev1.NodeExternalIP || addr.Type == corev1.NodeInternalIP { 248 | parsedIP := net.ParseIP(addr.Address) 249 | if parsedIP != nil { 250 | nodesIP = append(nodesIP, getIPNetFromIP(parsedIP)) 251 | } 252 | } 253 | } 254 | } 255 | 256 | return nodesIP, nil 257 | } 258 | 259 | func checkRulesUpdate(instance *rdbv1alpha1.RDBInstance, existingACLs *rdb.ListInstanceACLRulesResponse, nodesIP []net.IPNet) bool { 260 | needRulesUpdate := len(existingACLs.Rules) != len(nodesIP)+len(instance.Spec.ACL.Rules) 261 | 262 | if !needRulesUpdate { 263 | for _, existingRule := range existingACLs.Rules { 264 | foundRule := false 265 | for _, wantedRule := range instance.Spec.ACL.Rules { 266 | wantedRuleParsed := net.ParseIP(wantedRule.IPRange) 267 | if wantedRuleParsed != nil && existingRule.IP.String() == wantedRuleParsed.String() { 268 | foundRule = true 269 | break 270 | } 271 | } 272 | for _, nodeRule := range nodesIP { 273 | if foundRule { 274 | break 275 | } 276 | 277 | if nodeRule.String() == existingRule.IP.String() { 278 | foundRule = true 279 | } 280 | } 281 | if !foundRule { 282 | needRulesUpdate = true 283 | } 284 | } 285 | } 286 | 287 | return needRulesUpdate 288 | } 289 | 290 | func (m *InstanceManager) updateACLs(ctx context.Context, instance *rdbv1alpha1.RDBInstance, rdbInstance *rdb.Instance) error { 291 | existingACLs, err := m.API.ListInstanceACLRules(&rdb.ListInstanceACLRulesRequest{ 292 | Region: scw.Region(instance.Spec.Region), 293 | InstanceID: instance.Spec.InstanceID, 294 | }, scw.WithAllPages()) 295 | if err != nil { 296 | return err 297 | } 298 | 299 | var nodesIP []net.IPNet 300 | 301 | if instance.Spec.ACL.AllowCluster { 302 | nodesIP, err = m.getNodesIP(ctx) 303 | if err != nil { 304 | return err 305 | } 306 | } 307 | 308 | if checkRulesUpdate(instance, existingACLs, nodesIP) { 309 | rules := []*rdb.ACLRuleRequest{} 310 | for _, wantedRule := range instance.Spec.ACL.Rules { 311 | _, wantedRuleParsed, err := net.ParseCIDR(wantedRule.IPRange) 312 | if err != nil { 313 | m.Log.Error(err, "error parsing ip range, ignoring") 314 | continue 315 | } 316 | if wantedRuleParsed != nil { 317 | rules = append(rules, &rdb.ACLRuleRequest{ 318 | IP: scw.IPNet{ 319 | IPNet: *wantedRuleParsed, 320 | }, 321 | Description: wantedRule.Description, 322 | }) 323 | } 324 | } 325 | for _, nodeIP := range nodesIP { 326 | rules = append(rules, &rdb.ACLRuleRequest{ 327 | IP: scw.IPNet{ 328 | IPNet: nodeIP, 329 | }, 330 | Description: "Kuberentes node", 331 | }) 332 | } 333 | _, err = m.API.SetInstanceACLRules(&rdb.SetInstanceACLRulesRequest{ 334 | Region: scw.Region(instance.Spec.Region), 335 | InstanceID: instance.Spec.InstanceID, 336 | Rules: rules, 337 | }) 338 | if err != nil { 339 | return err 340 | } 341 | } 342 | 343 | return nil 344 | } 345 | 346 | func convertInstance(obj runtime.Object) (*rdbv1alpha1.RDBInstance, error) { 347 | instance, ok := obj.(*rdbv1alpha1.RDBInstance) 348 | if !ok { 349 | return nil, fmt.Errorf("failed type assertion on kind: %s", obj.GetObjectKind().GroupVersionKind().String()) 350 | } 351 | return instance, nil 352 | } 353 | 354 | func getIPNetFromIP(ip net.IP) net.IPNet { 355 | ipNet := net.IPNet{ 356 | IP: ip, 357 | } 358 | if ip.To4() != nil { 359 | ipNet.Mask = net.CIDRMask(32, 32) 360 | } else { 361 | ipNet.Mask = net.CIDRMask(128, 128) 362 | } 363 | return ipNet 364 | } 365 | 366 | func (m *InstanceManager) getInstanceFromIDAndRegion(ctx context.Context, instance *rdbv1alpha1.RDBInstance) (string, scw.Region, error) { 367 | instanceFrom := &rdbv1alpha1.RDBInstance{} 368 | 369 | if instance.Spec.InstanceFrom.Name != "" { 370 | instanceNamespace := instance.Spec.InstanceFrom.Namespace 371 | if instanceNamespace == "" { 372 | instanceNamespace = instance.Namespace 373 | } 374 | 375 | err := m.Get(ctx, client.ObjectKey{Name: instance.Spec.InstanceFrom.Name, Namespace: instanceNamespace}, instanceFrom) 376 | if err != nil { 377 | return "", "", err 378 | } 379 | 380 | return instanceFrom.Spec.InstanceID, scw.Region(instanceFrom.Spec.Region), nil 381 | } 382 | 383 | return instance.Spec.InstanceFrom.ExternalID, scw.Region(instance.Spec.InstanceFrom.Region), nil 384 | } 385 | -------------------------------------------------------------------------------- /pkg/manager/rdb/instance_webhook.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/scaleway/scaleway-sdk-go/api/rdb/v1" 7 | "github.com/scaleway/scaleway-sdk-go/scw" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/util/validation/field" 10 | ) 11 | 12 | // ValidateCreate validates the creation of a RDB Instance 13 | func (m *InstanceManager) ValidateCreate(ctx context.Context, obj runtime.Object) (field.ErrorList, error) { 14 | var allErrs field.ErrorList 15 | 16 | instance, err := convertInstance(obj) 17 | if err != nil { 18 | return nil, err 19 | } 20 | _, err = scw.ParseRegion(instance.Spec.Region) 21 | if instance.Spec.Region != "" && err != nil { 22 | allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("region"), instance.Spec.Region, "region is not valid")) 23 | return allErrs, nil // stop validation here since future calls will fail 24 | } 25 | 26 | enginesResp, err := m.API.ListDatabaseEngines(&rdb.ListDatabaseEnginesRequest{ 27 | Region: scw.Region(instance.Spec.Region), 28 | }, scw.WithAllPages()) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | if instance.Spec.InstanceID != "" { 34 | rdbInstance, err := m.API.GetInstance(&rdb.GetInstanceRequest{ 35 | Region: scw.Region(instance.Spec.Region), 36 | InstanceID: instance.Spec.InstanceID, 37 | }) 38 | if err != nil { 39 | allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("instanceID"), instance.Spec.InstanceID, err.Error())) 40 | return allErrs, nil 41 | } 42 | if instance.Spec.Engine != rdbInstance.Engine { 43 | allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("engine"), instance.Spec.Engine, "engine does not match")) 44 | } 45 | 46 | return allErrs, nil 47 | } 48 | 49 | engineFound := false 50 | for _, engine := range enginesResp.Engines { 51 | for _, engineVersion := range engine.Versions { 52 | if engineVersion.Name == instance.Spec.Engine { 53 | if engineVersion.Disabled { 54 | allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("engine"), instance.Spec.Engine, "engine is disabled")) 55 | } 56 | engineFound = true 57 | break 58 | } 59 | } 60 | } 61 | if !engineFound { 62 | allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("engine"), instance.Spec.Engine, "engine does not exist")) 63 | } 64 | 65 | nodeTypeErrs, err := m.checkNodeType(ctx, scw.Region(instance.Spec.Region), instance.Spec.NodeType) 66 | if err != nil { 67 | return nil, err 68 | } 69 | allErrs = append(allErrs, nodeTypeErrs...) 70 | 71 | return allErrs, nil 72 | } 73 | 74 | // ValidateUpdate validates the update of a RDB Instance 75 | func (m *InstanceManager) ValidateUpdate(ctx context.Context, oldObj runtime.Object, obj runtime.Object) (field.ErrorList, error) { 76 | var allErrs field.ErrorList 77 | 78 | instance, err := convertInstance(obj) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | oldInstance, err := convertInstance(oldObj) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | if oldInstance.Spec.InstanceID != "" && oldInstance.Spec.InstanceID != instance.Spec.InstanceID { 89 | allErrs = append(allErrs, field.Forbidden(field.NewPath("spec").Child("instanceID"), "field is immutable")) 90 | } 91 | 92 | if oldInstance.Spec.Region != "" && oldInstance.Spec.Region != instance.Spec.Region { 93 | allErrs = append(allErrs, field.Forbidden(field.NewPath("spec").Child("region"), "field is immutable")) 94 | } 95 | 96 | if oldInstance.Spec.Engine != instance.Spec.Engine { 97 | allErrs = append(allErrs, field.Forbidden(field.NewPath("spec").Child("engine"), "field is immutable")) 98 | } 99 | 100 | if oldInstance.Spec.IsHaCluster != instance.Spec.IsHaCluster && oldInstance.Spec.IsHaCluster { 101 | allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("isHaCluster"), instance.Spec.Engine, "HA instance can't be downgraded")) 102 | } 103 | 104 | if oldInstance.Spec.NodeType != instance.Spec.NodeType { 105 | nodeTypeErrs, err := m.checkNodeType(ctx, scw.Region(instance.Spec.Region), instance.Spec.NodeType) 106 | if err != nil { 107 | return nil, err 108 | } 109 | allErrs = append(allErrs, nodeTypeErrs...) 110 | } 111 | 112 | return allErrs, nil 113 | } 114 | 115 | func (m *InstanceManager) checkNodeType(ctx context.Context, region scw.Region, instanceNodeType string) (field.ErrorList, error) { 116 | var allErrs field.ErrorList 117 | 118 | nodeTypesResp, err := m.API.ListNodeTypes(&rdb.ListNodeTypesRequest{ 119 | Region: scw.Region(region), 120 | }, scw.WithAllPages()) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | nodeTypeFound := false 126 | for _, nodeType := range nodeTypesResp.NodeTypes { 127 | if nodeType.Name == instanceNodeType { 128 | if nodeType.Disabled { 129 | allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("nodeType"), instanceNodeType, "node type is disabled")) 130 | } 131 | nodeTypeFound = true 132 | break 133 | } 134 | } 135 | if !nodeTypeFound { 136 | allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("nodeType"), instanceNodeType, "node type does not exist")) 137 | } 138 | 139 | return allErrs, nil 140 | } 141 | -------------------------------------------------------------------------------- /pkg/manager/rdb/user.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/scaleway/scaleway-operator/pkg/manager/scaleway" 8 | "github.com/scaleway/scaleway-sdk-go/api/rdb/v1" 9 | "github.com/scaleway/scaleway-sdk-go/scw" 10 | corev1 "k8s.io/api/core/v1" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | "k8s.io/apimachinery/pkg/types" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | 15 | rdbv1alpha1 "github.com/scaleway/scaleway-operator/apis/rdb/v1alpha1" 16 | ) 17 | 18 | const ( 19 | // SecretPasswordKey is the key for accessing the pasword in the given secret 20 | SecretPasswordKey = "password" 21 | ) 22 | 23 | // UserManager manages the RDB users 24 | type UserManager struct { 25 | client.Client 26 | API *rdb.API 27 | scaleway.Manager 28 | } 29 | 30 | // Ensure reconciles the RDB user resource 31 | func (m *UserManager) Ensure(ctx context.Context, obj runtime.Object) (bool, error) { 32 | user, err := convertUser(obj) 33 | if err != nil { 34 | return false, err 35 | } 36 | 37 | instanceID, region, err := m.getInstanceIDAndRegion(ctx, user) 38 | if err != nil { 39 | return false, err 40 | } 41 | 42 | rdbUser, err := m.getByName(ctx, user) 43 | if err != nil { 44 | return false, err 45 | } 46 | 47 | if rdbUser != nil { 48 | if rdbUser.IsAdmin != user.Spec.Admin { 49 | _, err := m.API.UpdateUser(&rdb.UpdateUserRequest{ 50 | Region: region, 51 | InstanceID: instanceID, 52 | Name: rdbUser.Name, 53 | IsAdmin: scw.BoolPtr(user.Spec.Admin), 54 | }) 55 | if err != nil { 56 | return false, err 57 | } 58 | } 59 | // pass for now if password changed 60 | } else { 61 | password := "" 62 | 63 | if user.Spec.Password.ValueFrom != nil { 64 | secret := corev1.Secret{} 65 | err := m.Get(ctx, types.NamespacedName{ 66 | Name: user.Spec.Password.ValueFrom.SecretKeyRef.Name, 67 | Namespace: user.Spec.Password.ValueFrom.SecretKeyRef.Namespace, 68 | }, &secret) 69 | if err != nil { 70 | return false, err 71 | } 72 | password = string(secret.Data[SecretPasswordKey]) 73 | } else if user.Spec.Password.Value != nil { 74 | password = *user.Spec.Password.Value 75 | } 76 | 77 | rdbUser, err = m.API.CreateUser(&rdb.CreateUserRequest{ 78 | Region: region, 79 | InstanceID: instanceID, 80 | IsAdmin: user.Spec.Admin, 81 | Name: user.Spec.UserName, 82 | Password: password, 83 | }) 84 | if err != nil { 85 | return false, err 86 | } 87 | } 88 | 89 | return false, nil 90 | } 91 | 92 | // Delete deletes the RDB user resource 93 | func (m *UserManager) Delete(ctx context.Context, obj runtime.Object) (bool, error) { 94 | user, err := convertUser(obj) 95 | if err != nil { 96 | return false, err 97 | } 98 | 99 | instanceID, region, err := m.getInstanceIDAndRegion(ctx, user) 100 | if err != nil { 101 | return false, err 102 | } 103 | 104 | if instanceID == "" { 105 | return true, nil 106 | } 107 | 108 | err = m.API.DeleteUser(&rdb.DeleteUserRequest{ 109 | Region: region, 110 | InstanceID: instanceID, 111 | Name: user.Spec.UserName, 112 | }) 113 | if err != nil { 114 | if _, ok := err.(*scw.ResourceNotFoundError); ok { 115 | return true, nil 116 | } 117 | return false, err 118 | } 119 | 120 | return false, nil 121 | } 122 | 123 | // GetOwners returns the owners of the RDB user resource 124 | func (m *UserManager) GetOwners(ctx context.Context, obj runtime.Object) ([]scaleway.Owner, error) { 125 | user, err := convertUser(obj) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | if user.Spec.InstanceRef.Name == "" { 131 | return nil, nil 132 | } 133 | 134 | instance := &rdbv1alpha1.RDBInstance{} 135 | 136 | userNamespace := user.Spec.InstanceRef.Namespace 137 | if userNamespace == "" { 138 | userNamespace = user.Namespace 139 | } 140 | 141 | err = m.Get(ctx, client.ObjectKey{Name: user.Spec.InstanceRef.Name, Namespace: userNamespace}, instance) 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | return []scaleway.Owner{ 147 | { 148 | Key: types.NamespacedName{ 149 | Name: instance.Name, 150 | Namespace: instance.Namespace, 151 | }, 152 | Object: &rdbv1alpha1.RDBInstance{}, 153 | }, 154 | }, nil 155 | } 156 | 157 | func (m *UserManager) getInstanceIDAndRegion(ctx context.Context, user *rdbv1alpha1.RDBUser) (string, scw.Region, error) { 158 | instance := &rdbv1alpha1.RDBInstance{} 159 | 160 | if user.Spec.InstanceRef.Name != "" { 161 | userNamespace := user.Spec.InstanceRef.Namespace 162 | if userNamespace == "" { 163 | userNamespace = user.Namespace 164 | } 165 | 166 | err := m.Get(ctx, client.ObjectKey{Name: user.Spec.InstanceRef.Name, Namespace: userNamespace}, instance) 167 | if err != nil { 168 | return "", "", err 169 | } 170 | 171 | return instance.Spec.InstanceID, scw.Region(instance.Spec.Region), nil 172 | } 173 | 174 | return user.Spec.InstanceRef.ExternalID, scw.Region(user.Spec.InstanceRef.Region), nil 175 | } 176 | 177 | func (m *UserManager) getByName(ctx context.Context, user *rdbv1alpha1.RDBUser) (*rdb.User, error) { 178 | instanceID, region, err := m.getInstanceIDAndRegion(ctx, user) 179 | if err != nil { 180 | return nil, err 181 | } 182 | 183 | usersResp, err := m.API.ListUsers(&rdb.ListUsersRequest{ 184 | Region: region, 185 | InstanceID: instanceID, 186 | Name: scw.StringPtr(user.Spec.UserName), 187 | }, scw.WithAllPages()) 188 | if err != nil { 189 | if _, ok := err.(*scw.ResourceNotFoundError); ok { 190 | return nil, nil 191 | } 192 | return nil, err 193 | } 194 | 195 | var finalUser *rdb.User 196 | 197 | for _, rdbUser := range usersResp.Users { 198 | if rdbUser.Name == user.Spec.UserName { 199 | finalUser = rdbUser 200 | break 201 | } 202 | } 203 | 204 | if finalUser != nil { 205 | return finalUser, nil 206 | } 207 | 208 | return nil, nil 209 | } 210 | 211 | func convertUser(obj runtime.Object) (*rdbv1alpha1.RDBUser, error) { 212 | user, ok := obj.(*rdbv1alpha1.RDBUser) 213 | if !ok { 214 | return nil, fmt.Errorf("failed type assertion on kind: %s", obj.GetObjectKind().GroupVersionKind().String()) 215 | } 216 | return user, nil 217 | } 218 | -------------------------------------------------------------------------------- /pkg/manager/scaleway/manager.go: -------------------------------------------------------------------------------- 1 | package scaleway 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/runtime" 7 | "k8s.io/apimachinery/pkg/types" 8 | "k8s.io/apimachinery/pkg/util/validation/field" 9 | ) 10 | 11 | // Owner represent an owner of a Scaleway resource 12 | type Owner struct { 13 | Key types.NamespacedName 14 | Object runtime.Object 15 | } 16 | 17 | // Manager is the interface implemented by all Scaleway products 18 | type Manager interface { 19 | // Ensure is the method to implement to reconcile the resource 20 | Ensure(context.Context, runtime.Object) (bool, error) 21 | // Delete is the method to implement to delete the resource 22 | Delete(context.Context, runtime.Object) (bool, error) 23 | // GetOwners is the method to implement to get the resource's owners 24 | GetOwners(context.Context, runtime.Object) ([]Owner, error) 25 | 26 | // Webhooks 27 | // ValidateCreate is the method to implement for the creation validation 28 | ValidateCreate(context.Context, runtime.Object) (field.ErrorList, error) 29 | // ValidateUpdate is the method to implement for the update validation 30 | ValidateUpdate(context.Context, runtime.Object, runtime.Object) (field.ErrorList, error) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/utils/labels.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // LabelsToTags transform labels into tags 4 | func LabelsToTags(labels map[string]string) []string { 5 | tags := []string{} 6 | for key, value := range labels { 7 | tags = append(tags, key+"="+value) 8 | } 9 | return tags 10 | } 11 | 12 | // CompareTagsLabels returns true if the tags and labels are equal 13 | func CompareTagsLabels(tags []string, labels map[string]string) bool { 14 | if len(tags) != len(labels) { 15 | return false 16 | } 17 | for key, value := range labels { 18 | found := false 19 | for _, tag := range tags { 20 | if tag == key+"="+value { 21 | found = true 22 | } 23 | } 24 | if !found { 25 | return false 26 | } 27 | } 28 | return true 29 | } 30 | -------------------------------------------------------------------------------- /webhooks/rdb/rdbdatabase_webhook.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 webhooks 18 | 19 | import ( 20 | "context" 21 | "net/http" 22 | 23 | "github.com/go-logr/logr" 24 | admissionv1beta1 "k8s.io/api/admission/v1beta1" 25 | apierrors "k8s.io/apimachinery/pkg/api/errors" 26 | "k8s.io/apimachinery/pkg/runtime/schema" 27 | "k8s.io/apimachinery/pkg/util/validation/field" 28 | ctrl "sigs.k8s.io/controller-runtime" 29 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 30 | "sigs.k8s.io/controller-runtime/pkg/webhook" 31 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 32 | 33 | rdbv1alpha1 "github.com/scaleway/scaleway-operator/apis/rdb/v1alpha1" 34 | "github.com/scaleway/scaleway-operator/webhooks" 35 | ) 36 | 37 | // +kubebuilder:webhook:verbs=create;update,path=/validate-rdb-scaleway-com-v1alpha1-rdbdatabase,mutating=false,failurePolicy=fail,groups=rdb.scaleway.com,resources=rdbdatabases,versions=v1alpha1,name=vrdbdatabase.kb.io 38 | 39 | // RDBDatabaseValidator is the struct used to validate a RDBDatabase 40 | type RDBDatabaseValidator struct { 41 | ScalewayWebhook *webhooks.ScalewayWebhook 42 | *admission.Decoder 43 | Log logr.Logger 44 | } 45 | 46 | // SetupWebhookWithManager registers the RDBDatabase webhook 47 | func (v *RDBDatabaseValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { 48 | webhookServer := mgr.GetWebhookServer() 49 | webhookType, err := apiutil.GVKForObject(&rdbv1alpha1.RDBDatabase{}, mgr.GetScheme()) 50 | if err != nil { 51 | return err 52 | } 53 | webhookServer.Register(webhooks.GenerateValidatePath(webhookType), &webhook.Admission{ 54 | Handler: v, 55 | }) 56 | return nil 57 | 58 | } 59 | 60 | // Handle handles the main logic of the RDBDatabase webhook 61 | func (v *RDBDatabaseValidator) Handle(ctx context.Context, req admission.Request) admission.Response { 62 | instance := &rdbv1alpha1.RDBDatabase{} 63 | 64 | err := v.Decode(req, instance) 65 | if err != nil { 66 | return admission.Errored(http.StatusBadRequest, err) 67 | } 68 | 69 | var allErrs field.ErrorList 70 | 71 | switch req.Operation { 72 | case admissionv1beta1.Create: 73 | allErrs, err = v.ScalewayWebhook.ValidateCreate(ctx, instance) 74 | if err != nil { 75 | v.Log.Error(err, "could not validate rdb instance creation") 76 | return admission.Errored(http.StatusInternalServerError, err) 77 | } 78 | 79 | case admissionv1beta1.Update: 80 | oldDatabase := &rdbv1alpha1.RDBDatabase{} 81 | err = v.DecodeRaw(req.OldObject, oldDatabase) 82 | if err != nil { 83 | return admission.Errored(http.StatusBadRequest, err) 84 | } 85 | allErrs, err = v.ScalewayWebhook.ValidateUpdate(ctx, oldDatabase, instance) 86 | if err != nil { 87 | v.Log.Error(err, "could not validate rdb instance update") 88 | return admission.Errored(http.StatusInternalServerError, err) 89 | } 90 | } 91 | 92 | if len(allErrs) == 0 { 93 | return admission.Allowed("") 94 | } 95 | 96 | err = apierrors.NewInvalid(schema.GroupKind{Group: "rdb.scaleway.com", Kind: "RDBDatabase"}, instance.Name, allErrs) 97 | 98 | return admission.Denied(err.Error()) 99 | } 100 | 101 | // InjectDecoder injects the decoder. 102 | func (v *RDBDatabaseValidator) InjectDecoder(d *admission.Decoder) error { 103 | v.Decoder = d 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /webhooks/rdb/rdbinstance_webhook.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 webhooks 18 | 19 | import ( 20 | "context" 21 | "net/http" 22 | 23 | "github.com/go-logr/logr" 24 | admissionv1beta1 "k8s.io/api/admission/v1beta1" 25 | apierrors "k8s.io/apimachinery/pkg/api/errors" 26 | "k8s.io/apimachinery/pkg/runtime/schema" 27 | "k8s.io/apimachinery/pkg/util/validation/field" 28 | ctrl "sigs.k8s.io/controller-runtime" 29 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 30 | "sigs.k8s.io/controller-runtime/pkg/webhook" 31 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 32 | 33 | rdbv1alpha1 "github.com/scaleway/scaleway-operator/apis/rdb/v1alpha1" 34 | "github.com/scaleway/scaleway-operator/webhooks" 35 | ) 36 | 37 | // +kubebuilder:webhook:verbs=create;update,path=/validate-rdb-scaleway-com-v1alpha1-rdbinstance,mutating=false,failurePolicy=fail,groups=rdb.scaleway.com,resources=rdbinstances,versions=v1alpha1,name=vrdbinstance.kb.io 38 | 39 | // RDBInstanceValidator is the struct used to validate a RDBInstance 40 | type RDBInstanceValidator struct { 41 | ScalewayWebhook *webhooks.ScalewayWebhook 42 | *admission.Decoder 43 | Log logr.Logger 44 | } 45 | 46 | // SetupWebhookWithManager registers the RDBInstance webhook 47 | func (v *RDBInstanceValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { 48 | webhookServer := mgr.GetWebhookServer() 49 | webhookType, err := apiutil.GVKForObject(&rdbv1alpha1.RDBInstance{}, mgr.GetScheme()) 50 | if err != nil { 51 | return err 52 | } 53 | webhookServer.Register(webhooks.GenerateValidatePath(webhookType), &webhook.Admission{ 54 | Handler: v, 55 | }) 56 | return nil 57 | } 58 | 59 | // Handle handles the main logic of the RDBInstance webhook 60 | func (v *RDBInstanceValidator) Handle(ctx context.Context, req admission.Request) admission.Response { 61 | instance := &rdbv1alpha1.RDBInstance{} 62 | 63 | err := v.Decode(req, instance) 64 | if err != nil { 65 | return admission.Errored(http.StatusBadRequest, err) 66 | } 67 | 68 | var allErrs field.ErrorList 69 | 70 | switch req.Operation { 71 | case admissionv1beta1.Create: 72 | allErrs, err = v.ScalewayWebhook.ValidateCreate(ctx, instance) 73 | if err != nil { 74 | v.Log.Error(err, "could not validate rdb instance creation") 75 | return admission.Errored(http.StatusInternalServerError, err) 76 | } 77 | 78 | case admissionv1beta1.Update: 79 | oldInstance := &rdbv1alpha1.RDBInstance{} 80 | err = v.DecodeRaw(req.OldObject, oldInstance) 81 | if err != nil { 82 | return admission.Errored(http.StatusBadRequest, err) 83 | } 84 | allErrs, err = v.ScalewayWebhook.ValidateUpdate(ctx, oldInstance, instance) 85 | if err != nil { 86 | v.Log.Error(err, "could not validate rdb instance update") 87 | return admission.Errored(http.StatusInternalServerError, err) 88 | } 89 | } 90 | 91 | if len(allErrs) == 0 { 92 | return admission.Allowed("") 93 | } 94 | 95 | err = apierrors.NewInvalid(schema.GroupKind{Group: "rdb.scaleway.com", Kind: "RDBInstance"}, instance.Name, allErrs) 96 | 97 | return admission.Denied(err.Error()) 98 | } 99 | 100 | // InjectDecoder injects the decoder. 101 | func (v *RDBInstanceValidator) InjectDecoder(d *admission.Decoder) error { 102 | v.Decoder = d 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /webhooks/scaleway_webhook.go: -------------------------------------------------------------------------------- 1 | package webhooks 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/scaleway/scaleway-operator/pkg/manager/scaleway" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | "k8s.io/apimachinery/pkg/util/validation/field" 9 | ) 10 | 11 | // ScalewayWebhook is the base webhook for Scaleway products 12 | type ScalewayWebhook struct { 13 | ScalewayManager scaleway.Manager 14 | } 15 | 16 | // ValidateCreate calls the ValidateCreate method of the given resource 17 | func (r *ScalewayWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (field.ErrorList, error) { 18 | return r.ScalewayManager.ValidateCreate(ctx, obj) 19 | } 20 | 21 | // ValidateUpdate calls the ValidateUpdate method of the given resource 22 | func (r *ScalewayWebhook) ValidateUpdate(ctx context.Context, oldObj runtime.Object, obj runtime.Object) (field.ErrorList, error) { 23 | return r.ScalewayManager.ValidateUpdate(ctx, oldObj, obj) 24 | } 25 | -------------------------------------------------------------------------------- /webhooks/utils.go: -------------------------------------------------------------------------------- 1 | package webhooks 2 | 3 | import ( 4 | "strings" 5 | 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | ) 8 | 9 | // GenerateValidatePath returns the path for this validating webhook 10 | func GenerateValidatePath(gvk schema.GroupVersionKind) string { 11 | return "/validate-" + strings.Replace(gvk.Group, ".", "-", -1) + "-" + 12 | gvk.Version + "-" + strings.ToLower(gvk.Kind) 13 | } 14 | -------------------------------------------------------------------------------- /webhooks/utils_test.go: -------------------------------------------------------------------------------- 1 | package webhooks 2 | 3 | import ( 4 | "testing" 5 | 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | ) 8 | 9 | func Test_GenerateValidatePath(t *testing.T) { 10 | cases := []struct { 11 | gvk schema.GroupVersionKind 12 | output string 13 | }{ 14 | { 15 | schema.GroupVersionKind{ 16 | Group: "foo", 17 | Version: "v2", 18 | Kind: "bar", 19 | }, 20 | "/validate-foo-v2-bar", 21 | }, 22 | { 23 | schema.GroupVersionKind{ 24 | Group: "my-awesome-group", 25 | Version: "v99", 26 | Kind: "MyNiceKind", 27 | }, 28 | "/validate-my-awesome-group-v99-mynicekind", 29 | }, 30 | } 31 | 32 | for _, c := range cases { 33 | output := GenerateValidatePath(c.gvk) 34 | if output != c.output { 35 | t.Errorf("Got %s instead of %s", output, c.output) 36 | } 37 | } 38 | } 39 | --------------------------------------------------------------------------------