├── .dockerignore ├── .github └── workflows │ ├── actionlint.yml │ ├── codeql-analysis.yml │ ├── github-pages.yml │ ├── pre-commit.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CLAUDE.md ├── CONTRIBUTING.md ├── Dockerfile ├── Makefile ├── PROJECT ├── README.md ├── api └── v1alpha1 │ ├── groupversion_info.go │ ├── mysql_types.go │ ├── mysqldb_types.go │ ├── mysqluser_types.go │ └── zz_generated.deepcopy.go ├── aqua.yaml ├── cmd └── main.go ├── codecov.yml ├── config ├── crd │ ├── bases │ │ ├── mysql.nakamasato.com_mysqldbs.yaml │ │ ├── mysql.nakamasato.com_mysqls.yaml │ │ └── mysql.nakamasato.com_mysqlusers.yaml │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── patches │ │ ├── cainjection_in_mysqldbs.yaml │ │ ├── cainjection_in_mysqls.yaml │ │ ├── cainjection_in_mysqlusers.yaml │ │ ├── webhook_in_mysqldbs.yaml │ │ ├── webhook_in_mysqls.yaml │ │ └── webhook_in_mysqlusers.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ ├── manager_config_patch.yaml │ └── manager_gcp_sa_secret_patch.yaml ├── install │ ├── kustomization.yaml │ └── manager.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── mysql │ ├── kustomization.yaml │ ├── mysql-deployment.yaml │ ├── mysql-secret.yaml │ ├── mysql-service-nodeport.yaml │ └── mysql-service.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 │ ├── mysql_editor_role.yaml │ ├── mysql_viewer_role.yaml │ ├── mysqldb_editor_role.yaml │ ├── mysqldb_viewer_role.yaml │ ├── mysqluser_editor_role.yaml │ ├── mysqluser_viewer_role.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml ├── sample-migrations │ ├── 01_create_table.down.sql │ └── 01_create_table.up.sql ├── samples-on-k8s-with-gcp-secretmanager │ ├── kustomization.yaml │ ├── mysql_v1alpha1_mysql.yaml │ ├── mysql_v1alpha1_mysqldb.yaml │ └── mysql_v1alpha1_mysqluser.yaml ├── samples-on-k8s-with-k8s-secret │ ├── kustomization.yaml │ ├── mysql_v1alpha1_mysql.yaml │ ├── mysql_v1alpha1_mysqldb.yaml │ └── mysql_v1alpha1_mysqluser.yaml ├── samples-on-k8s │ ├── kustomization.yaml │ ├── mysql_v1alpha1_mysql.yaml │ ├── mysql_v1alpha1_mysqldb.yaml │ └── mysql_v1alpha1_mysqluser.yaml ├── samples-wtih-gcp-secretmanager │ ├── kustomization.yaml │ ├── mysql_v1alpha1_mysql.yaml │ ├── mysql_v1alpha1_mysqldb.yaml │ └── mysql_v1alpha1_mysqluser.yaml └── samples │ ├── kustomization.yaml │ ├── mysql_v1alpha1_mysql.yaml │ ├── mysql_v1alpha1_mysqldb.yaml │ └── mysql_v1alpha1_mysqluser.yaml ├── docs ├── developer-guide │ ├── api-resources.md │ ├── debug.md │ ├── helm.md │ ├── reconciliation.drawio.svg │ ├── reconciliation.md │ ├── testing.md │ ├── tools.md │ └── versions.md ├── diagram.drawio.svg ├── index.md ├── prometheus-graph.png ├── prometheus.png ├── run-local-kubernetes.drawio.svg ├── run-local-with-gcp-secretmanager.drawio.svg ├── run-local.drawio.svg └── usage │ ├── gcp-secretmanager.md │ ├── install-with-helm.md │ └── schema-migration.md ├── e2e ├── e2e_test.go ├── kind-config.yml ├── kind.go ├── skaffold.go ├── skaffold.yaml └── suite_test.go ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── internal ├── controller │ ├── mysql_controller.go │ ├── mysql_controller_test.go │ ├── mysqldb_controller.go │ ├── mysqldb_controller_test.go │ ├── mysqluser_controller.go │ ├── mysqluser_controller_test.go │ ├── suite_test.go │ └── test_utils.go ├── metrics │ ├── metrics.go │ └── metrics_test.go ├── mysql │ └── mysql.go ├── secret │ ├── gcp_secret_manager.go │ ├── kubernetes_secret_manager.go │ ├── raw_secret_manager.go │ └── secret_manager.go └── utils │ ├── utils.go │ └── utils_test.go ├── kuttl-test.yaml ├── mkdocs.yml ├── renovate.json ├── skaffold.yaml └── tests └── e2e └── with-valid-mysql ├── 00-assert.yaml ├── 00-mysql-deployment.yaml ├── 00-mysql-service.yaml ├── 01-assert.yaml └── 01-create-mysql-user.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | 6 | # ignore git files 7 | .git/ 8 | .gitignore 9 | .github/ 10 | 11 | # ignore docker-compose files 12 | docker-compose.*.yml 13 | 14 | # ignore environment files 15 | .env* 16 | 17 | # ignore document files 18 | .docs/ 19 | *.md 20 | 21 | # ignore editor settings 22 | .idea/ 23 | .vscode/ 24 | 25 | # ignore log files 26 | *.log 27 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | name: actionlint 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - ".github/workflows/*" 8 | 9 | jobs: 10 | actionlint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: actionlint 15 | run: | 16 | bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 17 | ./actionlint -color 18 | shell: bash 19 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | schedule: 16 | - cron: '27 7 * * 4' 17 | 18 | jobs: 19 | analyze: 20 | name: Analyze 21 | runs-on: ubuntu-latest 22 | permissions: 23 | actions: read 24 | contents: read 25 | security-events: write 26 | 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | language: [ 'go' ] 31 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 32 | # Learn more: 33 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 34 | 35 | steps: 36 | - name: Checkout repository 37 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v3 42 | with: 43 | languages: ${{ matrix.language }} 44 | # If you wish to specify custom queries, you can do so here or in a config file. 45 | # By default, queries listed here will override any specified in a config file. 46 | # Prefix the list here with "+" to use these queries and those in the config file. 47 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 48 | 49 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 50 | # If this step fails, then you should remove it and run the build manually (see below) 51 | - name: Autobuild 52 | uses: github/codeql-action/autobuild@v3 53 | 54 | # ℹ️ Command-line programs to run using the OS shell. 55 | # 📚 https://git.io/JvXDl 56 | 57 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 58 | # and modify them (or add more) to build your code if your project 59 | # uses a compiled language 60 | 61 | #- run: | 62 | # make bootstrap 63 | # make release 64 | 65 | - name: Perform CodeQL Analysis 66 | uses: github/codeql-action/analyze@v3 67 | -------------------------------------------------------------------------------- /.github/workflows/github-pages.yml: -------------------------------------------------------------------------------- 1 | name: github-pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 12 | - uses: actions/setup-python@v5 13 | with: 14 | python-version: 3.x 15 | - run: pip install mkdocs-material 16 | - run: mkdocs gh-deploy --force 17 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | pre-commit: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 10 | - uses: actions/setup-python@v5 11 | with: 12 | python-version: 3.x 13 | - uses: pre-commit/action@v3.0.1 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | jobs: 13 | build-and-push-image: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 22 | 23 | - name: Log in to the Container registry 24 | uses: docker/login-action@v3 25 | with: 26 | registry: ${{ env.REGISTRY }} 27 | username: ${{ github.actor }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Extract metadata (tags, labels) for Docker 31 | id: meta 32 | uses: docker/metadata-action@v5 33 | with: 34 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 35 | 36 | - name: Build and push Docker image 37 | uses: docker/build-push-action@v6 38 | with: 39 | context: . 40 | push: true 41 | tags: ${{ steps.meta.outputs.tags }} 42 | labels: ${{ steps.meta.outputs.labels }} 43 | 44 | create-pr-to-update-kustomization: 45 | needs: [build-and-push-image] 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Checkout repository 49 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 50 | with: 51 | ref: 'main' 52 | 53 | - name: Update kustomization 54 | run: IMG=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${GITHUB_REF:10} make update-version-to-install 55 | 56 | - name: Set output 57 | id: set-output 58 | run: echo "RELEASE_VERSION=${GITHUB_REF:10}" >> "$GITHUB_OUTPUT" 59 | 60 | - name: Create Pull Request 61 | uses: peter-evans/create-pull-request@v7 62 | with: 63 | token: ${{ secrets.GITHUB_TOKEN }} 64 | title: "chore: bump version to ${{ steps.set-output.outputs.RELEASE_VERSION }} in install/kustomization.yaml" 65 | body: | 66 | # Why 67 | - New version [${{ steps.set-output.outputs.RELEASE_VERSION }}](${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ steps.set-output.outputs.RELEASE_VERSION }}) was released. 68 | # What 69 | - Update kustomization for installation 70 | 71 | create-pr-for-helm-charts: 72 | needs: [build-and-push-image] 73 | runs-on: ubuntu-latest 74 | steps: 75 | - name: Checkout repository 76 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 77 | with: 78 | ref: main 79 | repository: nakamasato/helm-charts 80 | 81 | - name: Set output 82 | id: set-output 83 | run: echo "RELEASE_VERSION=${GITHUB_REF:10}" >> "$GITHUB_OUTPUT" 84 | 85 | - name: Update app version 86 | run: | 87 | yq e -i ".version = \"${{ steps.set-output.outputs.RELEASE_VERSION }}\"" charts/mysql-operator/Chart.yaml 88 | yq e -i ".appVersion = \"${{ steps.set-output.outputs.RELEASE_VERSION }}\"" charts/mysql-operator/Chart.yaml 89 | yq e -i ".controllerManager.manager.image.tag = \"${{ steps.set-output.outputs.RELEASE_VERSION }}\"" charts/mysql-operator/values.yaml 90 | 91 | - name: Create Pull Request 92 | uses: peter-evans/create-pull-request@v7 93 | with: 94 | token: ${{ secrets.PAT_TO_UPDATE_HELM_CHARTS_REPO }} # when expired, need to update in https://github.com/settings/tokens 95 | title: "chore: bump mysql-operator version to ${{ steps.set-output.outputs.RELEASE_VERSION }}" 96 | branch: bump-mysql-operator-chart 97 | body: | 98 | # Why 99 | - New version [${{ steps.set-output.outputs.RELEASE_VERSION }}](${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ steps.set-output.outputs.RELEASE_VERSION }}) was released. 100 | # What 101 | - bump mysql-operator chart version to [${{ steps.set-output.outputs.RELEASE_VERSION }}](${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ steps.set-output.outputs.RELEASE_VERSION }}) 102 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | path-filter: 11 | outputs: 12 | go: ${{steps.changes.outputs.go}} 13 | e2e: ${{steps.changes.outputs.e2e}} 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 17 | 18 | - uses: dorny/paths-filter@v3 19 | id: changes 20 | with: 21 | filters: | 22 | go: 23 | - '**.go' 24 | - 'go.*' 25 | - .github/workflows/test.yml 26 | e2e: 27 | - Dockerfile 28 | - .github/workflows/test.yml 29 | - config/crd/** 30 | - tests/e2e/** 31 | - internal/** 32 | - '**.go' 33 | - 'go.*' 34 | - kuttl-test.yaml 35 | - skaffold.yaml 36 | 37 | status-check: 38 | runs-on: ubuntu-latest 39 | needs: 40 | - lint 41 | - test 42 | - e2e-kuttl 43 | - e2e-ginkgo 44 | permissions: {} 45 | if: failure() 46 | steps: 47 | - run: exit 1 48 | 49 | lint: 50 | needs: path-filter 51 | if: needs.path-filter.outputs.go == 'true' 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 55 | 56 | - name: setup go 57 | uses: actions/setup-go@v5 58 | with: 59 | go-version-file: go.mod 60 | 61 | - name: Get golangci-lint version from aqua.yaml 62 | id: get-golangci-lint-version 63 | run: | 64 | GOLANGCI_LINT_VERSION=$(grep "golangci/golangci-lint" aqua.yaml | sed -E 's/.*golangci\/golangci-lint@(v[0-9]+\.[0-9]+\.[0-9]+).*/\1/') 65 | echo "Found golangci-lint version: ${GOLANGCI_LINT_VERSION}" 66 | echo "version=${GOLANGCI_LINT_VERSION}" >> "${GITHUB_OUTPUT}" 67 | 68 | - name: golangci-lint 69 | uses: golangci/golangci-lint-action@v8 70 | with: 71 | # Use version from aqua.yaml 72 | version: ${{ steps.get-golangci-lint-version.outputs.version }} 73 | 74 | # Optional: golangci-lint command line arguments. 75 | args: --timeout=3m # --issues-exit-code=0 76 | 77 | test: 78 | needs: path-filter 79 | if: needs.path-filter.outputs.go == 'true' 80 | runs-on: ubuntu-latest 81 | steps: 82 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 83 | 84 | - name: setup go 85 | uses: actions/setup-go@v5 86 | with: 87 | go-version-file: ./go.mod 88 | 89 | - run: | 90 | make test 91 | cat cover.out >> coverage.txt 92 | 93 | - name: codecov 94 | uses: codecov/codecov-action@v5 95 | 96 | e2e-kuttl: 97 | needs: path-filter 98 | if: needs.path-filter.outputs.e2e == 'true' 99 | runs-on: ubuntu-latest 100 | steps: 101 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 102 | 103 | - name: setup go 104 | uses: actions/setup-go@v5 105 | with: 106 | go-version-file: ./go.mod 107 | cache: true 108 | 109 | # https://krew.sigs.k8s.io/docs/user-guide/setup/install/ 110 | - name: krew - install 111 | run: | 112 | ( 113 | set -x; cd "$(mktemp -d)" && 114 | OS="$(uname | tr '[:upper:]' '[:lower:]')" && 115 | ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')" && 116 | KREW="krew-${OS}_${ARCH}" && 117 | curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/${KREW}.tar.gz" && 118 | tar zxvf "${KREW}.tar.gz" && 119 | ./"${KREW}" install krew 120 | ) 121 | 122 | # https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#adding-a-system-path 123 | - name: krew - set PATH 124 | run: echo "${KREW_ROOT:-$HOME/.krew}/bin:$PATH" >> "$GITHUB_PATH" 125 | 126 | # https://kuttl.dev/docs/cli.html#setup-the-kuttl-kubectl-plugin 127 | - name: kuttl - install 128 | run: | 129 | kubectl krew install kuttl 130 | kubectl kuttl -v 131 | 132 | - name: Set up Docker Buildx 133 | uses: docker/setup-buildx-action@v3 134 | 135 | - name: Build with gha 136 | uses: docker/build-push-action@v6 137 | with: 138 | context: . 139 | push: false # a shorthand for --output=type=registry if set to true 140 | load: true # a shorthand for --output=type=docker if set to true 141 | tags: mysql-operator:latest 142 | cache-from: type=gha 143 | cache-to: type=gha,mode=max 144 | 145 | - name: kuttl test 146 | run: make kuttl 147 | 148 | e2e-ginkgo: 149 | needs: path-filter 150 | if: needs.path-filter.outputs.e2e == 'true' 151 | runs-on: ubuntu-latest 152 | steps: 153 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 154 | 155 | - name: setup go 156 | uses: actions/setup-go@v5 157 | with: 158 | go-version-file: ./go.mod 159 | cache: true 160 | 161 | - name: install skaffold # TODO: #69 Enable to install skaffold in e2e 162 | run: | 163 | curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64 && \ 164 | sudo install skaffold /usr/local/bin/ 165 | 166 | - name: create kind cluster 167 | working-directory: e2e 168 | run: kind create cluster --name mysql-operator-e2e --kubeconfig kubeconfig --config kind-config.yml --wait 30s 169 | 170 | - name: skaffold run 171 | working-directory: e2e 172 | run: skaffold run --kubeconfig kubeconfig 173 | 174 | - name: e2e-with-ginkgo 175 | run: make e2e-with-ginkgo 176 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Kubernetes Generated files - skip generated files, except for vendored files 18 | 19 | !vendor/**/zz_generated.* 20 | 21 | # editor and IDE paraphernalia 22 | .idea 23 | *.swp 24 | *.swo 25 | *~ 26 | 27 | # kind logs 28 | kind-logs-* 29 | 30 | # kubeconfig 31 | kubeconfig 32 | sa-private-key.json 33 | 34 | **/.claude/settings.local.json 35 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v3.2.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | args: [--allow-multiple-documents] 11 | exclude: ^chart/templates/ 12 | - id: check-added-large-files 13 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Core Commands 6 | 7 | ### Building and Testing 8 | 9 | ```bash 10 | # Build the operator binary 11 | make build 12 | 13 | # Run the operator locally 14 | make run 15 | 16 | # Run unit tests 17 | make test 18 | 19 | # Run tests with specific focus 20 | bin/ginkgo -skip-package=e2e --focus "Should have finalizer" --fail-fast ./... 21 | 22 | # Run e2e tests with Ginkgo 23 | make e2e-with-ginkgo 24 | 25 | # Run e2e tests with KUTTL 26 | make e2e-with-kuttl 27 | 28 | # Run linting 29 | make lint 30 | 31 | # Generate CRD manifests and code 32 | make manifests generate 33 | ``` 34 | 35 | ### Deployment 36 | 37 | ```bash 38 | # Install CRDs into the K8s cluster 39 | make install 40 | 41 | # Uninstall CRDs from the K8s cluster 42 | make uninstall 43 | 44 | # Deploy controller to the K8s cluster 45 | make deploy 46 | 47 | # Undeploy controller from the K8s cluster 48 | make undeploy 49 | 50 | # Build Docker image with the manager 51 | make docker-build 52 | 53 | # Push Docker image with the manager 54 | make docker-push 55 | 56 | # Deploy using Skaffold (local development) 57 | skaffold dev 58 | ``` 59 | 60 | ### Working with Helm Charts 61 | 62 | ```bash 63 | # Generate Helm chart from kustomize configs 64 | make helm 65 | ``` 66 | 67 | ## Architecture Overview 68 | 69 | This is a Kubernetes operator built with [operator-sdk](https://sdk.operatorframework.io/) that manages MySQL databases, schema, users, and permissions in existing MySQL servers. It does not manage MySQL clusters themselves. 70 | 71 | ### Custom Resources 72 | 73 | 1. **MySQL**: Defines the connection to a MySQL server (host, port, admin credentials) 74 | 2. **MySQLUser**: Defines a MySQL user for a specific MySQL instance 75 | 3. **MySQLDB**: Defines a MySQL database for a specific MySQL instance, with optional schema migration 76 | 77 | ### Controllers 78 | 79 | 1. **MySQLReconciler**: Manages `MySQLClients` based on `MySQL` and `MySQLDB` resources 80 | 2. **MySQLUserReconciler**: Creates/deletes MySQL users defined in `MySQLUser` and creates Kubernetes Secrets to store passwords 81 | 3. **MySQLDBReconciler**: Creates/deletes databases and handles schema migrations defined in `MySQLDB` 82 | 83 | ### Key Components 84 | 85 | - **MySQLClients**: Manages database connections to MySQL servers 86 | - **SecretManagers**: Interface for retrieving secrets (raw, GCP Secret Manager, Kubernetes Secrets) 87 | - **Metrics**: Exposes Prometheus metrics for MySQL user operations 88 | 89 | ## Secret Management 90 | 91 | The operator supports three methods for handling admin credentials: 92 | 93 | 1. **Raw**: Plaintext credentials in the CR spec 94 | 2. **GCP Secret Manager**: Credentials stored in Google Cloud Secret Manager 95 | 3. **Kubernetes Secrets**: Credentials stored in Kubernetes Secrets 96 | 97 | Set the secret type when starting the operator: 98 | 99 | ```bash 100 | # For GCP Secret Manager 101 | go run ./cmd/main.go --admin-user-secret-type=gcp --gcp-project-id=$PROJECT_ID 102 | 103 | # For Kubernetes Secrets 104 | go run ./cmd/main.go --admin-user-secret-type=k8s --k8s-secret-namespace=default 105 | ``` 106 | 107 | ## Database Migrations 108 | 109 | The operator supports schema migrations through the `MySQLDB` custom resource using the golang-migrate library. Migrations can be sourced from GitHub repositories. 110 | 111 | Example configuration: 112 | 113 | ```yaml 114 | apiVersion: mysql.nakamasato.com/v1alpha1 115 | kind: MySQLDB 116 | metadata: 117 | name: sample-db 118 | spec: 119 | dbName: sample_db 120 | mysqlName: mysql-sample 121 | schemaMigrationFromGitHub: 122 | owner: myorg 123 | repo: myrepo 124 | path: migrations 125 | ref: main 126 | ``` 127 | 128 | ## Development Workflow 129 | 130 | 1. Start a local Kubernetes cluster (kind, minikube, etc.) 131 | 2. Run a MySQL server (either locally with Docker or in the cluster) 132 | 3. Install CRDs and run the operator 133 | 4. Apply sample custom resources 134 | 5. Validate the resources were created in MySQL 135 | 136 | ## Monitoring 137 | 138 | The operator exports Prometheus metrics: 139 | - `mysql_user_created_total` 140 | - `mysql_user_deleted_total` 141 | 142 | To view metrics, you can set up Prometheus and ServiceMonitor. 143 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.24 AS builder 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | # cache deps before building and copying source so that we don't need to re-download as much 11 | # and so that source changes don't invalidate our downloaded layer 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY cmd/main.go cmd/main.go 16 | COPY api/ api/ 17 | COPY internal/ internal/ 18 | 19 | # Build 20 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 21 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 22 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 23 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 24 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go 25 | 26 | # Use distroless as minimal base image to package the manager binary 27 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 28 | FROM gcr.io/distroless/static:nonroot 29 | WORKDIR / 30 | COPY --from=builder /workspace/manager . 31 | USER 65532:65532 32 | 33 | ENTRYPOINT ["/manager"] 34 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Image URL to use all building/pushing image targets 3 | IMG ?= ghcr.io/nakamasato/mysql-operator 4 | # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. 5 | ENVTEST_K8S_VERSION = 1.28.0 # 1.28.0 bumped in https://github.com/kubernetes-sigs/controller-runtime/releases/tag/v0.17.0 6 | ENVTEST_VERSION = release-0.20 7 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 8 | ifeq (,$(shell go env GOBIN)) 9 | GOBIN=$(shell go env GOPATH)/bin 10 | else 11 | GOBIN=$(shell go env GOBIN) 12 | endif 13 | 14 | # CONTAINER_TOOL defines the container tool to be used for building images. 15 | # Be aware that the target commands are only tested with Docker which is 16 | # scaffolded by default. However, you might want to replace it to use other 17 | # tools. (i.e. podman) 18 | CONTAINER_TOOL ?= docker 19 | 20 | # Setting SHELL to bash allows bash commands to be executed by recipes. 21 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 22 | SHELL = /usr/bin/env bash -o pipefail 23 | .SHELLFLAGS = -ec 24 | 25 | .PHONY: all 26 | all: build 27 | 28 | ##@ General 29 | 30 | # The help target prints out all targets with their descriptions organized 31 | # beneath their categories. The categories are represented by '##@' and the 32 | # target descriptions by '##'. The awk commands is responsible for reading the 33 | # entire set of makefiles included in this invocation, looking for lines of the 34 | # file as xyz: ## something, and then pretty-format the target and help. Then, 35 | # if there's a line with ##@ something, that gets pretty-printed as a category. 36 | # More info on the usage of ANSI control characters for terminal formatting: 37 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 38 | # More info on the awk command: 39 | # http://linuxcommand.org/lc3_adv_awk.php 40 | 41 | .PHONY: help 42 | help: ## Display this help. 43 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 44 | 45 | ##@ Development 46 | 47 | .PHONY: manifests 48 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 49 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 50 | 51 | .PHONY: generate 52 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 53 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 54 | 55 | .PHONY: fmt 56 | fmt: ## Run go fmt against code. 57 | go fmt ./... 58 | 59 | .PHONY: vet 60 | vet: ## Run go vet against code. 61 | go vet ./... 62 | 63 | .PHONY: test 64 | test: manifests generate fmt vet envtest ginkgo ## Run tests. 65 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" $(GINKGO) -cover -coverprofile cover.out -covermode=atomic -skip-package=e2e ./... 66 | 67 | ##@ Build 68 | 69 | .PHONY: build 70 | build: manifests generate fmt vet ## Build manager binary. 71 | go build -o bin/manager cmd/main.go 72 | 73 | .PHONY: run 74 | run: manifests generate fmt vet ## Run a controller from your host. 75 | go run ./cmd/main.go 76 | 77 | # If you wish built the manager image targeting other platforms you can use the --platform flag. 78 | # (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it. 79 | # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 80 | .PHONY: docker-build 81 | docker-build: test ## Build docker image with the manager. 82 | $(CONTAINER_TOOL) build -t ${IMG} . 83 | 84 | .PHONY: docker-push 85 | docker-push: ## Push docker image with the manager. 86 | $(CONTAINER_TOOL) push ${IMG} 87 | 88 | # PLATFORMS defines the target platforms for the manager image be build to provide support to multiple 89 | # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: 90 | # - able to use docker buildx . More info: https://docs.docker.com/build/buildx/ 91 | # - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 92 | # - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=> then the export will fail) 93 | # To properly provided solutions that supports more than one platform you should use this option. 94 | PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le 95 | .PHONY: docker-buildx 96 | docker-buildx: test ## Build and push docker image for the manager for cross-platform support 97 | # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile 98 | sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross 99 | - $(CONTAINER_TOOL) buildx create --name project-v3-builder 100 | $(CONTAINER_TOOL) buildx use project-v3-builder 101 | - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . 102 | - $(CONTAINER_TOOL) buildx rm project-v3-builder 103 | rm Dockerfile.cross 104 | 105 | ##@ Deployment 106 | 107 | ifndef ignore-not-found 108 | ignore-not-found = false 109 | endif 110 | 111 | .PHONY: install 112 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 113 | $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - 114 | 115 | .PHONY: uninstall 116 | uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 117 | $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - 118 | 119 | .PHONY: deploy 120 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 121 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 122 | $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - 123 | 124 | .PHONY: undeploy 125 | undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 126 | $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - 127 | 128 | ##@ Build Dependencies 129 | 130 | ## Location to install dependencies to 131 | LOCALBIN ?= $(shell pwd)/bin 132 | $(LOCALBIN): 133 | mkdir -p $(LOCALBIN) 134 | 135 | ## Tool Binaries 136 | KUBECTL ?= kubectl 137 | KUSTOMIZE ?= $(LOCALBIN)/kustomize 138 | CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen 139 | ENVTEST ?= $(LOCALBIN)/setup-envtest 140 | GINKGO ?= $(LOCALBIN)/ginkgo 141 | 142 | ## Tool Versions 143 | KUSTOMIZE_VERSION ?= v5.0.1 144 | CONTROLLER_TOOLS_VERSION ?= v0.14.0 145 | # Extract Ginkgo version from go.mod to keep it consistent 146 | GINKGO_VERSION ?= $(shell grep -m 1 'github.com/onsi/ginkgo/v2' go.mod | sed -E 's/.*github.com\/onsi\/ginkgo\/v2 (v[0-9]+\.[0-9]+\.[0-9]+).*/\1/') 147 | 148 | .PHONY: kustomize 149 | kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. 150 | $(KUSTOMIZE): $(LOCALBIN) 151 | @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ 152 | echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \ 153 | rm -rf $(LOCALBIN)/kustomize; \ 154 | fi 155 | test -s $(LOCALBIN)/kustomize || GOBIN=$(LOCALBIN) GO111MODULE=on go install sigs.k8s.io/kustomize/kustomize/v5@$(KUSTOMIZE_VERSION) 156 | 157 | .PHONY: controller-gen 158 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. 159 | $(CONTROLLER_GEN): $(LOCALBIN) 160 | test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ 161 | GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) 162 | 163 | .PHONY: envtest 164 | envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. 165 | $(ENVTEST): $(LOCALBIN) 166 | test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@$(ENVTEST_VERSION) 167 | 168 | .PHONY: ginkgo 169 | ginkgo: 170 | test -s $(LOCALBIN)/ginkgo && $(LOCALBIN)/ginkgo version | grep -q $(GINKGO_VERSION) || \ 171 | GOBIN=$(LOCALBIN) go install github.com/onsi/ginkgo/v2/ginkgo@$(GINKGO_VERSION) 172 | 173 | kuttl: 174 | kubectl kuttl test 175 | 176 | .PHONY: e2e-with-kuttl 177 | e2e-with-kuttl: 178 | docker build -t mysql-operator . 179 | kubectl kuttl test 180 | 181 | .PHONY: e2e-with-ginkgo 182 | e2e-with-ginkgo: ginkgo 183 | $(GINKGO) e2e 184 | 185 | update-version-to-install: kustomize 186 | cd config/install && $(KUSTOMIZE) edit set image controller=${IMG} 187 | 188 | HELMIFY ?= $(LOCALBIN)/helmify 189 | 190 | .PHONY: helmify 191 | helmify: $(HELMIFY) ## Download helmify locally if necessary. 192 | $(HELMIFY): $(LOCALBIN) 193 | test -s $(LOCALBIN)/helmify || GOBIN=$(LOCALBIN) go install github.com/arttor/helmify/cmd/helmify@latest 194 | 195 | helm: manifests kustomize helmify 196 | $(KUSTOMIZE) build config/install | $(HELMIFY) 197 | 198 | .PHONY: lint 199 | lint: 200 | golangci-lint run ./... 201 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: nakamasato.com 6 | layout: 7 | - go.kubebuilder.io/v4 8 | projectName: mysql-operator 9 | repo: github.com/nakamasato/mysql-operator 10 | resources: 11 | - api: 12 | crdVersion: v1 13 | namespaced: true 14 | controller: true 15 | domain: nakamasato.com 16 | group: mysql 17 | kind: MySQLUser 18 | path: github.com/nakamasato/mysql-operator/api/v1alpha1 19 | version: v1alpha1 20 | - api: 21 | crdVersion: v1 22 | namespaced: true 23 | controller: true 24 | domain: nakamasato.com 25 | group: mysql 26 | kind: MySQL 27 | path: github.com/nakamasato/mysql-operator/api/v1alpha1 28 | version: v1alpha1 29 | - api: 30 | crdVersion: v1 31 | namespaced: true 32 | controller: true 33 | domain: nakamasato.com 34 | group: mysql 35 | kind: MySQLDB 36 | path: github.com/nakamasato/mysql-operator/api/v1alpha1 37 | version: v1alpha1 38 | version: "3" 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MySQL Operator 2 | 3 | [![codecov](https://codecov.io/gh/nakamasato/mysql-operator/branch/main/graph/badge.svg?token=AWM1SBTI19)](https://codecov.io/gh/nakamasato/mysql-operator) 4 | 5 | This is a go-based Kubernetes operator built with [operator-sdk](https://sdk.operatorframework.io/docs/building-operators/golang/), which manages MySQL databases, schema, users, permissions in existing MySQL servers. This operator DOES NOT manage MySQL cluster like other MySQL operators such as [vitess](https://github.com/vitessio/vitess), [mysql/mysql-operator](https://github.com/mysql/mysql-operator). 6 | 7 | ## Motivation 8 | 9 | Reduce human operations: 10 | 11 | 1. **User management**: When creating a MySQL user for an application running on Kubernetes, it's necessary to create a MySQL user and create a Secret manually or with a script, which can be replaced with a Kubernetes operator. The initial idea is from KafkaUser and KafkaTopic in [Strimzi Kafka Operator](https://github.com/strimzi/strimzi-kafka-operator). With a custom resource for MySQL user, we can manage MySQL users with Kubernetes manifest files as a part of dependent application. 12 | Benefits from such a custom resource and operator: 13 | 1. Kubernetes manifest files for an application and its dependent resources (including MySQL user) can be managed together with Kustomize or Helm chart, with which we can easily duplicate whole environment. 14 | 1. There's no chance to require someone to check the raw password as it's stored directly to Secret by the operator, and read by the dependent application from the Secret. 15 | 1. **Database migration**: Reduce manual operations but keep changelog. When any schema migration or database operation is required, we needed a human operation, which has potential risk of human errors that should be avoided. With a Kubernetes operator, we can execute each database operation in the standard way with traceable changlog. 16 | 17 | ## Versions 18 | 19 | - Go: 1.24.0 20 | 21 | ## Components 22 | 23 | ![](docs/diagram.drawio.svg) 24 | 25 | 1. Custom Resource 26 | 1. `MySQL`: MySQL connection (`host`, `port`, `adminUser`, `adminPassword` holding the credentials to connect to MySQL. `adminUser` and `adminPassword` can be given by GSM or k8s Secret other than plaintext.) 27 | 1. `MySQLUser`: MySQL user (`mysqlName` and `host`) 28 | 1. `MySQLDB`: MySQL database (`mysqlName`, `dbName`, `schemaMigrationFromGitHub`) 29 | 1. Reconciler 30 | 1. `MySQLReconciler` is responsible for managing `MySQLClients` based on `MySQL` and `MySQLDB` resources 31 | 1. `MySQLUserReconciler` is responsible for creating/deleting MySQL users defined in `MySQLUser` using `MySQLClients`, and creating Secret to store MySQL user's password 32 | 1. `MySQLDBReconciler` is responsible for creating/deleting database and schema migration defined in `MySQLDB` using `MySQLClients` 33 | 34 | ## Getting Started 35 | 36 | 1. Install (Create CRD and operator objects) 37 | With kustomize: 38 | ``` 39 | kubectl apply -k https://github.com/nakamasato/mysql-operator/config/install 40 | ``` 41 | With Helm: 42 | ``` 43 | helm repo add nakamasato https://nakamasato.github.io/helm-charts 44 | helm repo update 45 | helm install mysql-operator nakamasato/mysql-operator 46 | ``` 47 | 48 | 1. (Optional) prepare MySQL. 49 | ``` 50 | kubectl apply -k https://github.com/nakamasato/mysql-operator/config/mysql 51 | ``` 52 | 1. Apply custom resources (`MySQL`, `MySQLUser`, `MySQLDB`). 53 | 54 | `mysql.yaml` credentials to connect to the MySQL: 55 | 56 | ```yaml 57 | apiVersion: mysql.nakamasato.com/v1alpha1 58 | kind: MySQL 59 | metadata: 60 | name: mysql-sample 61 | spec: 62 | host: mysql.default # need to include namespace if you use Kubernetes Service as an endpoint. 63 | adminUser: 64 | name: root 65 | type: raw 66 | adminPassword: 67 | name: password 68 | type: raw 69 | ``` 70 | 71 | `mysqluser.yaml`: MySQL user 72 | 73 | ```yaml 74 | apiVersion: mysql.nakamasato.com/v1alpha1 75 | kind: MySQLUser 76 | metadata: 77 | name: sample-user 78 | spec: 79 | mysqlName: mysql-sample 80 | host: '%' 81 | ``` 82 | 83 | `mysqldb.yaml`: MySQL database 84 | 85 | ```yaml 86 | apiVersion: mysql.nakamasato.com/v1alpha1 87 | kind: MySQLDB 88 | metadata: 89 | name: sample-db # this is not a name for MySQL database but just a Kubernetes object name 90 | spec: 91 | dbName: sample_db # this is MySQL database name 92 | mysqlName: mysql-sample 93 | ``` 94 | 95 | ``` 96 | kubectl apply -k https://github.com/nakamasato/mysql-operator/config/samples-on-k8s 97 | ``` 98 | 1. Check `MySQLUser` and `Secret` for the MySQL user 99 | 100 | ``` 101 | kubectl get mysqluser 102 | NAME PHASE REASON 103 | sample-user Ready Both secret and mysql user are successfully created. 104 | ``` 105 | 106 | ``` 107 | kubectl get secret 108 | NAME TYPE DATA AGE 109 | mysql-mysql-sample-sample-user Opaque 1 10s 110 | ``` 111 | 1. Connect to MySQL with the secret 112 | ``` 113 | kubectl exec -it $(kubectl get po | grep mysql | head -1 | awk '{print $1}') -- mysql -usample-user -p$(kubectl get secret mysql-mysql-sample-nakamasato -o jsonpath='{.data.password}' | base64 --decode) 114 | ``` 115 | 1. Delete custom resources (`MySQL`, `MySQLUser`, `MySQLDB`). 116 | Example: 117 | ``` 118 | kubectl delete -k https://github.com/nakamasato/mysql-operator/config/samples-on-k8s 119 | ``` 120 | 121 |
NOTICE 122 | 123 | custom resources might get stuck if MySQL is deleted before (to be improved). → Remove finalizers to forcifully delete the stuck objects: 124 | ``` 125 | kubectl patch mysqluser -p '{"metadata":{"finalizers": []}}' --type=merge 126 | ``` 127 | ``` 128 | kubectl patch mysql -p '{"metadata":{"finalizers": []}}' --type=merge 129 | ``` 130 | 131 | ``` 132 | kubectl patch mysqldb -p '{"metadata":{"finalizers": []}}' --type=merge 133 | ``` 134 | 135 |
136 | 137 | 1. (Optional) Delete MySQL 138 | ``` 139 | kubectl delete -k https://github.com/nakamasato/mysql-operator/config/mysql 140 | ``` 141 | 1. Uninstall `mysql-operator` 142 | ``` 143 | kubectl delete -k https://github.com/nakamasato/mysql-operator/config/install 144 | ``` 145 | 146 | ## With GCP Secret Manager 147 | 148 | Instead of writing raw password in `MySQL.Spec.AdminPassword`, you can get the password for root user from an external secret manager (e.g. GCP) (ref: [Authenticate to Google Cloud using a service account](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity)) 149 | 150 | 1. Create `SecretManager` 151 | ``` 152 | echo -n "password" | gcloud secrets create mysql-password --data-file=- 153 | echo -n "root" | gcloud secrets create mysql-user --data-file=- 154 | ``` 155 | 1. Create a `Secret` for credentials json for **service account** with `roles/secretm 156 | anager.secretAccessor` permission 157 | ``` 158 | kubectl create secret generic gcp-sa-private-key --from-file=sa-private-key.json 159 | ``` 160 | 1. Install mysql-operator with `--set adminUserSecretType=gcp --set gcpProjectId=$PROJECT_ID` 161 | ``` 162 | helm repo add nakamasato https://nakamasato.github.io/helm-charts 163 | helm repo update 164 | helm install mysql-operator nakamasato/mysql-operator --set adminUserSecretType=gcp --set gcpProjectId=$PROJECT_ID 165 | ``` 166 | 1. You can specify `type: gcp` for `adminUser` and `adminPassword`. 167 | 168 | ```yaml 169 | apiVersion: mysql.nakamasato.com/v1alpha1 170 | kind: MySQL 171 | metadata: 172 | name: mysql-sample 173 | spec: 174 | host: mysql.default # need to include namespace if you use Kubernetes Service as an endpoint. 175 | adminUser: 176 | name: mysql-user # secret name in SecretManager 177 | type: gcp 178 | adminPassword: 179 | name: mysql-password # secret name in SecretManager 180 | type: gcp 181 | ``` 182 | 183 | Example: (you need to run `kubectl apply -k config/mysql`) 184 | ``` 185 | kubectl apply -k config/samples-on-k8s-with-gcp-secretmanager 186 | ``` 187 | 188 | [Read credentials from GCP SecretManager](docs/usage/gcp-secretmanager.md) 189 | 190 | 191 | ## With k8s Secret Manager 192 | 193 | Instead of writing raw password in `MySQL.Spec.AdminPassword`, you can get the password for root user from an external secret manager (e.g. K8s) 194 | 195 | 1. Create Kubernetes Secret. 196 | ``` 197 | kubectl create secret generic mysql-user --from-literal=key=root 198 | kubectl create secret generic mysql-password --from-literal=key=password 199 | ``` 200 | 201 | 1. Install mysql-operator with `--set adminUserSecretType=k8s --set adminUserSecretNamespace=default` 202 | ``` 203 | helm repo add nakamasato https://nakamasato.github.io/helm-charts 204 | helm repo update 205 | helm install mysql-operator nakamasato/mysql-operator --set adminUserSecretType=k8s --set adminUserSecretNamespace=default 206 | ``` 207 | 1. You can specify `type: k8s` for `adminUser` and `adminPassword`. 208 | 209 | ```yaml 210 | apiVersion: mysql.nakamasato.com/v1alpha1 211 | kind: MySQL 212 | metadata: 213 | name: mysql-sample 214 | spec: 215 | host: mysql.default # need to include namespace if you use Kubernetes Service as an endpoint. 216 | adminUser: 217 | name: mysql-user # secret name in SecretManager 218 | type: k8s 219 | adminPassword: 220 | name: mysql-password # secret name in SecretManager 221 | type: k8s 222 | ``` 223 | 224 | Example: (you need to run `kubectl apply -k config/mysql`) 225 | ``` 226 | kubectl apply -k config/samples-on-k8s-with-k8s-secret 227 | ``` 228 | 229 | 230 | ## Exposed Metrics 231 | 232 | - `mysql_user_created_total` 233 | - `mysql_user_deleted_total` 234 | ## Contributing 235 | 236 | [CONTRIBUTING](CONTRIBUTING.md) 237 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 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 mysql v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=mysql.nakamasato.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: "mysql.nakamasato.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 | -------------------------------------------------------------------------------- /api/v1alpha1/mysql_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 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 | "fmt" 21 | 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | ) 24 | 25 | // MySQLSpec holds the connection information for the target MySQL cluster. 26 | type MySQLSpec struct { 27 | 28 | // Host is MySQL host of target MySQL cluster. 29 | Host string `json:"host"` 30 | 31 | //+kubebuilder:default=3306 32 | 33 | // Port is MySQL port of target MySQL cluster. 34 | Port int16 `json:"port,omitempty"` 35 | 36 | // AdminUser is MySQL user to connect target MySQL cluster. 37 | AdminUser Secret `json:"adminUser"` 38 | 39 | // AdminPassword is MySQL password to connect target MySQL cluster. 40 | AdminPassword Secret `json:"adminPassword"` 41 | } 42 | 43 | // MySQLStatus defines the observed state of MySQL 44 | type MySQLStatus struct { 45 | // true if successfully connected to the MySQL cluster 46 | Connected bool `json:"connected,omitempty"` 47 | 48 | // Reason for connection failure 49 | Reason string `json:"reason,omitempty"` 50 | 51 | //+kubebuilder:default=0 52 | 53 | // The number of users in this MySQL 54 | UserCount int32 `json:"userCount"` 55 | 56 | //+kubebuilder:default=0 57 | 58 | // The number of database in this MySQL 59 | DBCount int32 `json:"dbCount"` 60 | } 61 | 62 | //+kubebuilder:object:root=true 63 | //+kubebuilder:subresource:status 64 | //+kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.host` 65 | //+kubebuilder:printcolumn:name="AdminUser",type=string,JSONPath=`.spec.adminUser.name` 66 | //+kubebuilder:printcolumn:name="Connected",type=boolean,JSONPath=`.status.connected` 67 | //+kubebuilder:printcolumn:name="UserCount",type="integer",JSONPath=".status.userCount",description="The number of MySQLUsers that belongs to the MySQL" 68 | //+kubebuilder:printcolumn:name="DBCount",type="integer",JSONPath=".status.dbCount",description="The number of MySQLDBs that belongs to the MySQL" 69 | //+kubebuilder:printcolumn:name="Reason",type=string,JSONPath=`.status.reason` 70 | 71 | // MySQL is the Schema for the mysqls API 72 | type MySQL struct { 73 | metav1.TypeMeta `json:",inline"` 74 | metav1.ObjectMeta `json:"metadata,omitempty"` 75 | 76 | Spec MySQLSpec `json:"spec,omitempty"` 77 | Status MySQLStatus `json:"status,omitempty"` 78 | } 79 | 80 | func (m MySQL) GetKey() string { 81 | return fmt.Sprintf("%s-%s", m.Namespace, m.Name) 82 | } 83 | 84 | //+kubebuilder:object:root=true 85 | 86 | // MySQLList contains a list of MySQL 87 | type MySQLList struct { 88 | metav1.TypeMeta `json:",inline"` 89 | metav1.ListMeta `json:"metadata,omitempty"` 90 | Items []MySQL `json:"items"` 91 | } 92 | 93 | type Secret struct { 94 | // Secret Name 95 | Name string `json:"name"` 96 | 97 | // +kubebuilder:validation:Enum=raw;gcp;k8s 98 | 99 | // Secret Type (e.g. gcp, raw, k8s) 100 | Type string `json:"type"` 101 | } 102 | 103 | func init() { 104 | SchemeBuilder.Register(&MySQL{}, &MySQLList{}) 105 | } 106 | -------------------------------------------------------------------------------- /api/v1alpha1/mysqldb_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 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 | "fmt" 21 | 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | ) 24 | 25 | // MySQLDBSpec defines the desired state of MySQLDB 26 | type MySQLDBSpec struct { 27 | 28 | // MySQL (CRD) name to reference to, which decides the destination MySQL server 29 | MysqlName string `json:"mysqlName"` 30 | 31 | // MySQL Database name 32 | DBName string `json:"dbName"` 33 | 34 | // MySQL Database Schema Migrations from GitHub 35 | SchemaMigrationFromGitHub *GitHubConfig `json:"schemaMigrationFromGitHub,omitempty"` 36 | } 37 | 38 | // MySQLDBStatus defines the observed state of MySQLDB 39 | type MySQLDBStatus struct { 40 | // The phase of database creation 41 | Phase string `json:"phase,omitempty"` 42 | 43 | // The reason for the current phase 44 | Reason string `json:"reason,omitempty"` 45 | 46 | // Schema Migration status 47 | SchemaMigration SchemaMigration `json:"schemaMigration,omitempty"` 48 | } 49 | 50 | //+kubebuilder:object:root=true 51 | //+kubebuilder:subresource:status 52 | //+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="The phase of MySQLDB" 53 | //+kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.reason",description="The reason for the current phase of this MySQLDB" 54 | //+kubebuilder:printcolumn:name="SchemaMigration",type="string",JSONPath=".status.schemaMigration",description="schema_migration table if schema migration is enabled." 55 | 56 | // MySQLDB is the Schema for the mysqldbs API 57 | type MySQLDB struct { 58 | metav1.TypeMeta `json:",inline"` 59 | metav1.ObjectMeta `json:"metadata,omitempty"` 60 | 61 | Spec MySQLDBSpec `json:"spec,omitempty"` 62 | Status MySQLDBStatus `json:"status,omitempty"` 63 | } 64 | 65 | func (m MySQLDB) GetKey() string { 66 | return fmt.Sprintf("%s-%s-%s", m.Namespace, m.Spec.MysqlName, m.Spec.DBName) 67 | } 68 | 69 | //+kubebuilder:object:root=true 70 | 71 | // MySQLDBList contains a list of MySQLDB 72 | type MySQLDBList struct { 73 | metav1.TypeMeta `json:",inline"` 74 | metav1.ListMeta `json:"metadata,omitempty"` 75 | Items []MySQLDB `json:"items"` 76 | } 77 | 78 | // GitHubConfig holds GitHub repo, path, and ref for Data Migration 79 | // https://github.com/golang-migrate/migrate/tree/master/source/github 80 | type GitHubConfig struct { 81 | Owner string `json:"owner"` 82 | Repo string `json:"repo"` 83 | Path string `json:"path"` 84 | Ref string `json:"ref,omitempty"` 85 | } 86 | 87 | func (c GitHubConfig) GetSourceUrl() string { 88 | baseUrl := fmt.Sprintf("github://%s/%s/%s", c.Owner, c.Repo, c.Path) 89 | if c.Ref == "" { 90 | return baseUrl 91 | 92 | } 93 | return fmt.Sprintf("%s#%s", baseUrl, c.Ref) 94 | } 95 | 96 | // This reflect the schema_migration table 97 | type SchemaMigration struct { 98 | Version uint `json:"version"` 99 | Dirty bool `json:"dirty"` 100 | } 101 | 102 | func init() { 103 | SchemeBuilder.Register(&MySQLDB{}, &MySQLDBList{}) 104 | } 105 | -------------------------------------------------------------------------------- /api/v1alpha1/mysqluser_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 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 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // MySQLUserSpec defines the desired state of MySQLUser 24 | type MySQLUserSpec struct { 25 | 26 | // MySQL (CRD) name to reference to, which decides the destination MySQL server 27 | MysqlName string `json:"mysqlName"` 28 | 29 | // +kubebuilder:default=% 30 | // +kubebuilder:validation:Optional 31 | 32 | // MySQL hostname for MySQL account 33 | Host string `json:"host"` 34 | } 35 | 36 | // MySQLUserStatus defines the observed state of MySQLUser 37 | type MySQLUserStatus struct { 38 | 39 | // +patchMergeKey=type 40 | // +patchStrategy=merge 41 | // +listType=map 42 | // +listMapKey=type 43 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` 44 | Phase string `json:"phase,omitempty"` 45 | Reason string `json:"reason,omitempty"` 46 | 47 | // +kubebuilder:default=false 48 | 49 | // true if MySQL user is created 50 | MySQLUserCreated bool `json:"mysql_user_created,omitempty"` 51 | 52 | // +kubebuilder:default=false 53 | 54 | // true if Secret is created 55 | SecretCreated bool `json:"secret_created,omitempty"` 56 | } 57 | 58 | func (m *MySQLUser) GetConditions() []metav1.Condition { 59 | return m.Status.Conditions 60 | } 61 | 62 | func (m *MySQLUser) SetConditions(conditions []metav1.Condition) { 63 | m.Status.Conditions = conditions 64 | } 65 | 66 | //+kubebuilder:object:root=true 67 | //+kubebuilder:subresource:status 68 | //+kubebuilder:printcolumn:name="MySQLUser",type="boolean",JSONPath=".status.mysql_user_created",description="true if MySQL user is created" 69 | //+kubebuilder:printcolumn:name="Secret",type="boolean",JSONPath=".status.secret_created",description="true if Secret is created" 70 | //+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="The phase of this MySQLUser" 71 | //+kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.reason",description="The reason for the current phase of this MySQLUser" 72 | 73 | // MySQLUser is the Schema for the mysqlusers API 74 | type MySQLUser struct { 75 | metav1.TypeMeta `json:",inline"` 76 | metav1.ObjectMeta `json:"metadata,omitempty"` 77 | 78 | Spec MySQLUserSpec `json:"spec,omitempty"` 79 | Status MySQLUserStatus `json:"status,omitempty"` 80 | } 81 | 82 | //+kubebuilder:object:root=true 83 | 84 | // MySQLUserList contains a list of MySQLUser 85 | type MySQLUserList struct { 86 | metav1.TypeMeta `json:",inline"` 87 | metav1.ListMeta `json:"metadata,omitempty"` 88 | Items []MySQLUser `json:"items"` 89 | } 90 | 91 | func init() { 92 | SchemeBuilder.Register(&MySQLUser{}, &MySQLUserList{}) 93 | } 94 | -------------------------------------------------------------------------------- /aqua.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # aqua - Declarative CLI Version Manager 3 | # https://aquaproj.github.io/ 4 | # checksum: 5 | # enabled: true 6 | # require_checksum: true 7 | # supported_envs: 8 | # - all 9 | registries: 10 | - type: standard 11 | ref: v4.375.0 # renovate: depName=aquaproj/aqua-registry 12 | packages: 13 | - name: golangci/golangci-lint@v2.1.6 14 | - name: golang/go@1.24.3 15 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 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 | "context" 21 | "flag" 22 | "os" 23 | 24 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 25 | // to ensure that exec-entrypoint and run can make use of them. 26 | 27 | "go.uber.org/zap/zapcore" 28 | _ "k8s.io/client-go/plugin/pkg/client/auth" 29 | 30 | "k8s.io/apimachinery/pkg/runtime" 31 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 32 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 33 | ctrl "sigs.k8s.io/controller-runtime" 34 | "sigs.k8s.io/controller-runtime/pkg/client" 35 | "sigs.k8s.io/controller-runtime/pkg/healthz" 36 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 37 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 38 | 39 | mysqlv1alpha1 "github.com/nakamasato/mysql-operator/api/v1alpha1" 40 | controllers "github.com/nakamasato/mysql-operator/internal/controller" 41 | "github.com/nakamasato/mysql-operator/internal/mysql" 42 | "github.com/nakamasato/mysql-operator/internal/secret" 43 | //+kubebuilder:scaffold:imports 44 | ) 45 | 46 | var ( 47 | scheme = runtime.NewScheme() 48 | setupLog = ctrl.Log.WithName("setup") 49 | ) 50 | 51 | func init() { 52 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 53 | 54 | utilruntime.Must(mysqlv1alpha1.AddToScheme(scheme)) 55 | //+kubebuilder:scaffold:scheme 56 | } 57 | 58 | func main() { 59 | var metricsAddr string 60 | var enableLeaderElection bool 61 | var probeAddr string 62 | var adminUserSecretType string 63 | var projectId string 64 | var secretNamespace string 65 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 66 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 67 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 68 | "Enable leader election for controller manager. "+ 69 | "Enabling this will ensure there is only one active controller manager.") 70 | flag.StringVar(&adminUserSecretType, "admin-user-secret-type", "", 71 | "The secret manager to get credentials from. "+ 72 | "Currently, support raw, gcp, and k8s. ") 73 | flag.StringVar(&projectId, "gcp-project-id", "", 74 | "GCP project id. Set this value to use adminUserSecretType=gcp. "+ 75 | "Also can be set by environment variable PROJECT_ID."+ 76 | "If both are set, the flag is used.") 77 | flag.StringVar(&secretNamespace, "k8s-secret-namespace", "", 78 | "Kubernetes namespace where MYSQL credentials secrets is located. Set this value to use adminUserSecretType=k8s. "+ 79 | "Also can be set by environment variable SECRET_NAMESPACE."+ 80 | "If both are set, the flag is used.") 81 | opts := zap.Options{ 82 | Development: true, 83 | TimeEncoder: zapcore.ISO8601TimeEncoder, 84 | } 85 | opts.BindFlags(flag.CommandLine) 86 | flag.Parse() 87 | 88 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 89 | 90 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 91 | Scheme: scheme, 92 | Metrics: metricsserver.Options{BindAddress: metricsAddr}, 93 | HealthProbeBindAddress: probeAddr, 94 | LeaderElection: enableLeaderElection, 95 | LeaderElectionID: "dfc6d3c2.nakamasato.com", 96 | }) 97 | if err != nil { 98 | setupLog.Error(err, "unable to start manager") 99 | os.Exit(1) 100 | } 101 | 102 | mysqlClients := mysql.MySQLClients{} 103 | 104 | if err = (&controllers.MySQLUserReconciler{ 105 | Client: mgr.GetClient(), 106 | Scheme: mgr.GetScheme(), 107 | MySQLClients: mysqlClients, 108 | }).SetupWithManager(mgr); err != nil { 109 | setupLog.Error(err, "unable to create controller", "controller", "MySQLUser") 110 | os.Exit(1) 111 | } 112 | 113 | ctx := context.Background() 114 | secretManagers := map[string]secret.SecretManager{ 115 | "raw": secret.RawSecretManager{}, 116 | } 117 | switch adminUserSecretType { 118 | case "gcp": 119 | if projectId == "" { 120 | projectId = os.Getenv("PROJECT_ID") 121 | } 122 | gcpSecretManager, err := secret.NewGCPSecretManager(ctx, projectId) 123 | if err != nil { 124 | setupLog.Error(err, "failed to initialize GCPSecretManager") 125 | os.Exit(1) 126 | } 127 | defer gcpSecretManager.Close() 128 | setupLog.Info("Initialized gcpSecretManager", "projectId", projectId) 129 | secretManagers["gcp"] = gcpSecretManager 130 | case "k8s": 131 | if secretNamespace == "" { 132 | secretNamespace = os.Getenv("SECRET_NAMESPACE") 133 | } 134 | k8sSecretManager, err := secret.Newk8sSecretManager(ctx, secretNamespace, mgr.GetClient()) 135 | if err != nil { 136 | setupLog.Error(err, "failed to initialize k8sSecretManager") 137 | os.Exit(1) 138 | } 139 | setupLog.Info("Initialized k8sSecretManager", "namespace", secretNamespace) 140 | secretManagers["k8s"] = k8sSecretManager 141 | } 142 | if err = (&controllers.MySQLReconciler{ 143 | Client: mgr.GetClient(), 144 | Scheme: mgr.GetScheme(), 145 | MySQLClients: mysqlClients, 146 | MySQLDriverName: "mysql", 147 | SecretManagers: secretManagers, 148 | }).SetupWithManager(mgr); err != nil { 149 | setupLog.Error(err, "unable to create controller", "controller", "MySQL") 150 | os.Exit(1) 151 | } 152 | if err = (&controllers.MySQLDBReconciler{ 153 | Client: mgr.GetClient(), 154 | Scheme: mgr.GetScheme(), 155 | MySQLClients: mysqlClients, 156 | }).SetupWithManager(mgr); err != nil { 157 | setupLog.Error(err, "unable to create controller", "controller", "MySQLDB") 158 | os.Exit(1) 159 | } 160 | 161 | // Set index for mysqluser with spec.mysqlName 162 | // this is necessary to get MySQLUser/MySQLDB that references a MySQL 163 | cache := mgr.GetCache() 164 | indexFunc := func(obj client.Object) []string { 165 | return []string{obj.(*mysqlv1alpha1.MySQLUser).Spec.MysqlName} 166 | } 167 | if err := cache.IndexField(context.TODO(), &mysqlv1alpha1.MySQLUser{}, "spec.mysqlName", indexFunc); err != nil { 168 | panic(err) 169 | } 170 | indexFunc = func(obj client.Object) []string { 171 | return []string{obj.(*mysqlv1alpha1.MySQLDB).Spec.MysqlName} 172 | } 173 | if err := cache.IndexField(context.TODO(), &mysqlv1alpha1.MySQLDB{}, "spec.mysqlName", indexFunc); err != nil { 174 | panic(err) 175 | } 176 | 177 | //+kubebuilder:scaffold:builder 178 | 179 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 180 | setupLog.Error(err, "unable to set up health check") 181 | os.Exit(1) 182 | } 183 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 184 | setupLog.Error(err, "unable to set up ready check") 185 | os.Exit(1) 186 | } 187 | 188 | setupLog.Info("starting manager") 189 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 190 | setupLog.Error(err, "problem running manager") 191 | os.Exit(1) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: true 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "50...100" 8 | status: 9 | project: 10 | default: 11 | target: 0% 12 | patch: 13 | default: 14 | target: 0% 15 | ignore: 16 | - e2e 17 | -------------------------------------------------------------------------------- /config/crd/bases/mysql.nakamasato.com_mysqldbs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.14.0 7 | name: mysqldbs.mysql.nakamasato.com 8 | spec: 9 | group: mysql.nakamasato.com 10 | names: 11 | kind: MySQLDB 12 | listKind: MySQLDBList 13 | plural: mysqldbs 14 | singular: mysqldb 15 | scope: Namespaced 16 | versions: 17 | - additionalPrinterColumns: 18 | - description: The phase of MySQLDB 19 | jsonPath: .status.phase 20 | name: Phase 21 | type: string 22 | - description: The reason for the current phase of this MySQLDB 23 | jsonPath: .status.reason 24 | name: Reason 25 | type: string 26 | - description: schema_migration table if schema migration is enabled. 27 | jsonPath: .status.schemaMigration 28 | name: SchemaMigration 29 | type: string 30 | name: v1alpha1 31 | schema: 32 | openAPIV3Schema: 33 | description: MySQLDB is the Schema for the mysqldbs API 34 | properties: 35 | apiVersion: 36 | description: |- 37 | APIVersion defines the versioned schema of this representation of an object. 38 | Servers should convert recognized schemas to the latest internal value, and 39 | may reject unrecognized values. 40 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 41 | type: string 42 | kind: 43 | description: |- 44 | Kind is a string value representing the REST resource this object represents. 45 | Servers may infer this from the endpoint the client submits requests to. 46 | Cannot be updated. 47 | In CamelCase. 48 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 49 | type: string 50 | metadata: 51 | type: object 52 | spec: 53 | description: MySQLDBSpec defines the desired state of MySQLDB 54 | properties: 55 | dbName: 56 | description: MySQL Database name 57 | type: string 58 | mysqlName: 59 | description: MySQL (CRD) name to reference to, which decides the destination 60 | MySQL server 61 | type: string 62 | schemaMigrationFromGitHub: 63 | description: MySQL Database Schema Migrations from GitHub 64 | properties: 65 | owner: 66 | type: string 67 | path: 68 | type: string 69 | ref: 70 | type: string 71 | repo: 72 | type: string 73 | required: 74 | - owner 75 | - path 76 | - repo 77 | type: object 78 | required: 79 | - dbName 80 | - mysqlName 81 | type: object 82 | status: 83 | description: MySQLDBStatus defines the observed state of MySQLDB 84 | properties: 85 | phase: 86 | description: The phase of database creation 87 | type: string 88 | reason: 89 | description: The reason for the current phase 90 | type: string 91 | schemaMigration: 92 | description: Schema Migration status 93 | properties: 94 | dirty: 95 | type: boolean 96 | version: 97 | type: integer 98 | required: 99 | - dirty 100 | - version 101 | type: object 102 | type: object 103 | type: object 104 | served: true 105 | storage: true 106 | subresources: 107 | status: {} 108 | -------------------------------------------------------------------------------- /config/crd/bases/mysql.nakamasato.com_mysqls.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.14.0 7 | name: mysqls.mysql.nakamasato.com 8 | spec: 9 | group: mysql.nakamasato.com 10 | names: 11 | kind: MySQL 12 | listKind: MySQLList 13 | plural: mysqls 14 | singular: mysql 15 | scope: Namespaced 16 | versions: 17 | - additionalPrinterColumns: 18 | - jsonPath: .spec.host 19 | name: Host 20 | type: string 21 | - jsonPath: .spec.adminUser.name 22 | name: AdminUser 23 | type: string 24 | - jsonPath: .status.connected 25 | name: Connected 26 | type: boolean 27 | - description: The number of MySQLUsers that belongs to the MySQL 28 | jsonPath: .status.userCount 29 | name: UserCount 30 | type: integer 31 | - description: The number of MySQLDBs that belongs to the MySQL 32 | jsonPath: .status.dbCount 33 | name: DBCount 34 | type: integer 35 | - jsonPath: .status.reason 36 | name: Reason 37 | type: string 38 | name: v1alpha1 39 | schema: 40 | openAPIV3Schema: 41 | description: MySQL is the Schema for the mysqls API 42 | properties: 43 | apiVersion: 44 | description: |- 45 | APIVersion defines the versioned schema of this representation of an object. 46 | Servers should convert recognized schemas to the latest internal value, and 47 | may reject unrecognized values. 48 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 49 | type: string 50 | kind: 51 | description: |- 52 | Kind is a string value representing the REST resource this object represents. 53 | Servers may infer this from the endpoint the client submits requests to. 54 | Cannot be updated. 55 | In CamelCase. 56 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 57 | type: string 58 | metadata: 59 | type: object 60 | spec: 61 | description: MySQLSpec holds the connection information for the target 62 | MySQL cluster. 63 | properties: 64 | adminPassword: 65 | description: AdminPassword is MySQL password to connect target MySQL 66 | cluster. 67 | properties: 68 | name: 69 | description: Secret Name 70 | type: string 71 | type: 72 | description: Secret Type (e.g. gcp, raw, k8s) 73 | enum: 74 | - raw 75 | - gcp 76 | - k8s 77 | type: string 78 | required: 79 | - name 80 | - type 81 | type: object 82 | adminUser: 83 | description: AdminUser is MySQL user to connect target MySQL cluster. 84 | properties: 85 | name: 86 | description: Secret Name 87 | type: string 88 | type: 89 | description: Secret Type (e.g. gcp, raw, k8s) 90 | enum: 91 | - raw 92 | - gcp 93 | - k8s 94 | type: string 95 | required: 96 | - name 97 | - type 98 | type: object 99 | host: 100 | description: Host is MySQL host of target MySQL cluster. 101 | type: string 102 | port: 103 | default: 3306 104 | description: Port is MySQL port of target MySQL cluster. 105 | type: integer 106 | required: 107 | - adminPassword 108 | - adminUser 109 | - host 110 | type: object 111 | status: 112 | description: MySQLStatus defines the observed state of MySQL 113 | properties: 114 | connected: 115 | description: true if successfully connected to the MySQL cluster 116 | type: boolean 117 | dbCount: 118 | default: 0 119 | description: The number of database in this MySQL 120 | format: int32 121 | type: integer 122 | reason: 123 | description: Reason for connection failure 124 | type: string 125 | userCount: 126 | default: 0 127 | description: The number of users in this MySQL 128 | format: int32 129 | type: integer 130 | required: 131 | - dbCount 132 | - userCount 133 | type: object 134 | type: object 135 | served: true 136 | storage: true 137 | subresources: 138 | status: {} 139 | -------------------------------------------------------------------------------- /config/crd/bases/mysql.nakamasato.com_mysqlusers.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.14.0 7 | name: mysqlusers.mysql.nakamasato.com 8 | spec: 9 | group: mysql.nakamasato.com 10 | names: 11 | kind: MySQLUser 12 | listKind: MySQLUserList 13 | plural: mysqlusers 14 | singular: mysqluser 15 | scope: Namespaced 16 | versions: 17 | - additionalPrinterColumns: 18 | - description: true if MySQL user is created 19 | jsonPath: .status.mysql_user_created 20 | name: MySQLUser 21 | type: boolean 22 | - description: true if Secret is created 23 | jsonPath: .status.secret_created 24 | name: Secret 25 | type: boolean 26 | - description: The phase of this MySQLUser 27 | jsonPath: .status.phase 28 | name: Phase 29 | type: string 30 | - description: The reason for the current phase of this MySQLUser 31 | jsonPath: .status.reason 32 | name: Reason 33 | type: string 34 | name: v1alpha1 35 | schema: 36 | openAPIV3Schema: 37 | description: MySQLUser is the Schema for the mysqlusers API 38 | properties: 39 | apiVersion: 40 | description: |- 41 | APIVersion defines the versioned schema of this representation of an object. 42 | Servers should convert recognized schemas to the latest internal value, and 43 | may reject unrecognized values. 44 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 45 | type: string 46 | kind: 47 | description: |- 48 | Kind is a string value representing the REST resource this object represents. 49 | Servers may infer this from the endpoint the client submits requests to. 50 | Cannot be updated. 51 | In CamelCase. 52 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 53 | type: string 54 | metadata: 55 | type: object 56 | spec: 57 | description: MySQLUserSpec defines the desired state of MySQLUser 58 | properties: 59 | host: 60 | default: '%' 61 | description: MySQL hostname for MySQL account 62 | type: string 63 | mysqlName: 64 | description: MySQL (CRD) name to reference to, which decides the destination 65 | MySQL server 66 | type: string 67 | required: 68 | - mysqlName 69 | type: object 70 | status: 71 | description: MySQLUserStatus defines the observed state of MySQLUser 72 | properties: 73 | conditions: 74 | items: 75 | description: "Condition contains details for one aspect of the current 76 | state of this API Resource.\n---\nThis struct is intended for 77 | direct use as an array at the field path .status.conditions. For 78 | example,\n\n\n\ttype FooStatus struct{\n\t // Represents the 79 | observations of a foo's current state.\n\t // Known .status.conditions.type 80 | are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // 81 | +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t 82 | \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" 83 | patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t 84 | \ // other fields\n\t}" 85 | properties: 86 | lastTransitionTime: 87 | description: |- 88 | lastTransitionTime is the last time the condition transitioned from one status to another. 89 | This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. 90 | format: date-time 91 | type: string 92 | message: 93 | description: |- 94 | message is a human readable message indicating details about the transition. 95 | This may be an empty string. 96 | maxLength: 32768 97 | type: string 98 | observedGeneration: 99 | description: |- 100 | observedGeneration represents the .metadata.generation that the condition was set based upon. 101 | For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date 102 | with respect to the current state of the instance. 103 | format: int64 104 | minimum: 0 105 | type: integer 106 | reason: 107 | description: |- 108 | reason contains a programmatic identifier indicating the reason for the condition's last transition. 109 | Producers of specific condition types may define expected values and meanings for this field, 110 | and whether the values are considered a guaranteed API. 111 | The value should be a CamelCase string. 112 | This field may not be empty. 113 | maxLength: 1024 114 | minLength: 1 115 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 116 | type: string 117 | status: 118 | description: status of the condition, one of True, False, Unknown. 119 | enum: 120 | - "True" 121 | - "False" 122 | - Unknown 123 | type: string 124 | type: 125 | description: |- 126 | type of condition in CamelCase or in foo.example.com/CamelCase. 127 | --- 128 | Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be 129 | useful (see .node.status.conditions), the ability to deconflict is important. 130 | The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) 131 | maxLength: 316 132 | pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 133 | type: string 134 | required: 135 | - lastTransitionTime 136 | - message 137 | - reason 138 | - status 139 | - type 140 | type: object 141 | type: array 142 | x-kubernetes-list-map-keys: 143 | - type 144 | x-kubernetes-list-type: map 145 | mysql_user_created: 146 | default: false 147 | description: true if MySQL user is created 148 | type: boolean 149 | phase: 150 | type: string 151 | reason: 152 | type: string 153 | secret_created: 154 | default: false 155 | description: true if Secret is created 156 | type: boolean 157 | type: object 158 | type: object 159 | served: true 160 | storage: true 161 | subresources: 162 | status: {} 163 | -------------------------------------------------------------------------------- /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/mysql.nakamasato.com_mysqls.yaml 6 | - bases/mysql.nakamasato.com_mysqldbs.yaml 7 | - bases/mysql.nakamasato.com_mysqlusers.yaml 8 | #+kubebuilder:scaffold:crdkustomizeresource 9 | 10 | patches: 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_mysqls.yaml 14 | #- patches/webhook_in_mysqldbs.yaml 15 | #- patches/webhook_in_mysqlusers.yaml 16 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 17 | 18 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 19 | # patches here are for enabling the CA injection for each CRD 20 | #- patches/cainjection_in_mysqls.yaml 21 | #- patches/cainjection_in_mysqldbs.yaml 22 | #- patches/cainjection_in_mysqlusers.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 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_mysqldbs.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 7 | name: mysqldbs.mysql.nakamasato.com 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_mysqls.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 7 | name: mysqls.mysql.nakamasato.com 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_mysqlusers.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 7 | name: mysqlusers.mysql.nakamasato.com 8 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_mysqldbs.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: mysqldbs.mysql.nakamasato.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_mysqls.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: mysqls.mysql.nakamasato.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_mysqlusers.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: mysqlusers.mysql.nakamasato.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: mysql-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: mysql-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | #labels: 13 | #- includeSelectors: true 14 | # pairs: 15 | # someName: someValue 16 | 17 | resources: 18 | - ../crd 19 | - ../rbac 20 | - ../manager 21 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 22 | # crd/kustomization.yaml 23 | #- ../webhook 24 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 25 | #- ../certmanager 26 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 27 | #- ../prometheus 28 | 29 | # [GCP SecretManager] Mount GCP service account key as secret 30 | # secretGenerator: 31 | # - name: gcp-sa-private-key 32 | # files: 33 | # - sa-private-key.json 34 | 35 | patchesStrategicMerge: 36 | # Protect the /metrics endpoint by putting it behind auth. 37 | # If you want your controller-manager to expose the /metrics 38 | # endpoint w/o any authn/z, please comment the following line. 39 | - manager_auth_proxy_patch.yaml 40 | 41 | # [GCP SecretManager] Mount GCP service account key as secret 42 | # - manager_gcp_sa_secret_patch.yaml 43 | 44 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 45 | # crd/kustomization.yaml 46 | #- manager_webhook_patch.yaml 47 | 48 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 49 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 50 | # 'CERTMANAGER' needs to be enabled to use ca injection 51 | #- webhookcainjection_patch.yaml 52 | 53 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 54 | # Uncomment the following replacements to add the cert-manager CA injection annotations 55 | #replacements: 56 | # - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs 57 | # kind: Certificate 58 | # group: cert-manager.io 59 | # version: v1 60 | # name: serving-cert # this name should match the one in certificate.yaml 61 | # fieldPath: .metadata.namespace # namespace of the certificate CR 62 | # targets: 63 | # - select: 64 | # kind: ValidatingWebhookConfiguration 65 | # fieldPaths: 66 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 67 | # options: 68 | # delimiter: '/' 69 | # index: 0 70 | # create: true 71 | # - select: 72 | # kind: MutatingWebhookConfiguration 73 | # fieldPaths: 74 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 75 | # options: 76 | # delimiter: '/' 77 | # index: 0 78 | # create: true 79 | # - select: 80 | # kind: CustomResourceDefinition 81 | # fieldPaths: 82 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 83 | # options: 84 | # delimiter: '/' 85 | # index: 0 86 | # create: true 87 | # - source: 88 | # kind: Certificate 89 | # group: cert-manager.io 90 | # version: v1 91 | # name: serving-cert # this name should match the one in certificate.yaml 92 | # fieldPath: .metadata.name 93 | # targets: 94 | # - select: 95 | # kind: ValidatingWebhookConfiguration 96 | # fieldPaths: 97 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 98 | # options: 99 | # delimiter: '/' 100 | # index: 1 101 | # create: true 102 | # - select: 103 | # kind: MutatingWebhookConfiguration 104 | # fieldPaths: 105 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 106 | # options: 107 | # delimiter: '/' 108 | # index: 1 109 | # create: true 110 | # - select: 111 | # kind: CustomResourceDefinition 112 | # fieldPaths: 113 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 114 | # options: 115 | # delimiter: '/' 116 | # index: 1 117 | # create: true 118 | # - source: # Add cert-manager annotation to the webhook Service 119 | # kind: Service 120 | # version: v1 121 | # name: webhook-service 122 | # fieldPath: .metadata.name # namespace of the service 123 | # targets: 124 | # - select: 125 | # kind: Certificate 126 | # group: cert-manager.io 127 | # version: v1 128 | # fieldPaths: 129 | # - .spec.dnsNames.0 130 | # - .spec.dnsNames.1 131 | # options: 132 | # delimiter: '.' 133 | # index: 0 134 | # create: true 135 | # - source: 136 | # kind: Service 137 | # version: v1 138 | # name: webhook-service 139 | # fieldPath: .metadata.namespace # namespace of the service 140 | # targets: 141 | # - select: 142 | # kind: Certificate 143 | # group: cert-manager.io 144 | # version: v1 145 | # fieldPaths: 146 | # - .spec.dnsNames.0 147 | # - .spec.dnsNames.1 148 | # options: 149 | # delimiter: '.' 150 | # index: 1 151 | # create: true 152 | -------------------------------------------------------------------------------- /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 | securityContext: 14 | allowPrivilegeEscalation: false 15 | capabilities: 16 | drop: 17 | - "ALL" 18 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.14.1 19 | args: 20 | - "--secure-listen-address=0.0.0.0:8443" 21 | - "--upstream=http://127.0.0.1:8080/" 22 | - "--logtostderr=true" 23 | - "--v=0" 24 | ports: 25 | - containerPort: 8443 26 | protocol: TCP 27 | name: https 28 | resources: 29 | limits: 30 | cpu: 500m 31 | memory: 128Mi 32 | requests: 33 | cpu: 5m 34 | memory: 64Mi 35 | - name: manager 36 | args: 37 | - "--health-probe-bind-address=:8081" 38 | - "--metrics-bind-address=127.0.0.1:8080" 39 | - "--leader-elect" 40 | -------------------------------------------------------------------------------- /config/default/manager_config_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 | -------------------------------------------------------------------------------- /config/default/manager_gcp_sa_secret_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: kube-rbac-proxy 11 | securityContext: 12 | allowPrivilegeEscalation: false 13 | capabilities: 14 | drop: 15 | - "ALL" 16 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.14.1 17 | args: 18 | - "--secure-listen-address=0.0.0.0:8443" 19 | - "--upstream=http://127.0.0.1:8080/" 20 | - "--logtostderr=true" 21 | - "--v=0" 22 | ports: 23 | - containerPort: 8443 24 | protocol: TCP 25 | name: https 26 | resources: 27 | limits: 28 | cpu: 500m 29 | memory: 128Mi 30 | requests: 31 | cpu: 5m 32 | memory: 64Mi 33 | - name: manager 34 | args: 35 | - "--admin-user-secret-type=gcp" 36 | - "--health-probe-bind-address=:8081" 37 | - "--metrics-bind-address=127.0.0.1:8080" 38 | - "--leader-elect" 39 | volumeMounts: 40 | - name: gcp-sa-private-key 41 | mountPath: /var/secrets/google 42 | env: 43 | - name: GOOGLE_APPLICATION_CREDENTIALS 44 | value: /var/secrets/google/sa-private-key.json 45 | volumes: 46 | - name: gcp-sa-private-key 47 | secret: 48 | secretName: gcp-sa-private-key 49 | -------------------------------------------------------------------------------- /config/install/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: mysql-operator-system 2 | namePrefix: mysql-operator- 3 | 4 | resources: 5 | - manager.yaml 6 | - ../crd 7 | - ../rbac 8 | 9 | generatorOptions: 10 | disableNameSuffixHash: true 11 | 12 | apiVersion: kustomize.config.k8s.io/v1beta1 13 | kind: Kustomization 14 | images: 15 | - name: controller 16 | newName: ghcr.io/nakamasato/mysql-operator 17 | newTag: v0.4.3 18 | -------------------------------------------------------------------------------- /config/install/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 | securityContext: 26 | runAsNonRoot: true 27 | containers: 28 | - command: 29 | - /manager 30 | args: 31 | - --leader-elect 32 | image: controller:latest 33 | imagePullPolicy: IfNotPresent 34 | name: manager 35 | securityContext: 36 | allowPrivilegeEscalation: false 37 | livenessProbe: 38 | httpGet: 39 | path: /healthz 40 | port: 8081 41 | initialDelaySeconds: 15 42 | periodSeconds: 20 43 | readinessProbe: 44 | httpGet: 45 | path: /readyz 46 | port: 8081 47 | initialDelaySeconds: 5 48 | periodSeconds: 10 49 | resources: 50 | limits: 51 | cpu: 200m 52 | memory: 100Mi 53 | requests: 54 | cpu: 100m 55 | memory: 20Mi 56 | serviceAccountName: controller-manager 57 | terminationGracePeriodSeconds: 10 58 | -------------------------------------------------------------------------------- /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: mysql-operator 8 | newTag: latest 9 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: namespace 7 | app.kubernetes.io/instance: system 8 | app.kubernetes.io/component: manager 9 | app.kubernetes.io/created-by: mysql-operator 10 | app.kubernetes.io/part-of: mysql-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: system 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: controller-manager 18 | namespace: system 19 | labels: 20 | control-plane: controller-manager 21 | app.kubernetes.io/name: deployment 22 | app.kubernetes.io/instance: controller-manager 23 | app.kubernetes.io/component: manager 24 | app.kubernetes.io/created-by: mysql-operator 25 | app.kubernetes.io/part-of: mysql-operator 26 | app.kubernetes.io/managed-by: kustomize 27 | spec: 28 | selector: 29 | matchLabels: 30 | control-plane: controller-manager 31 | replicas: 1 32 | template: 33 | metadata: 34 | annotations: 35 | kubectl.kubernetes.io/default-container: manager 36 | labels: 37 | control-plane: controller-manager 38 | spec: 39 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 40 | # according to the platforms which are supported by your solution. 41 | # It is considered best practice to support multiple architectures. You can 42 | # build your manager image using the makefile target docker-buildx. 43 | # affinity: 44 | # nodeAffinity: 45 | # requiredDuringSchedulingIgnoredDuringExecution: 46 | # nodeSelectorTerms: 47 | # - matchExpressions: 48 | # - key: kubernetes.io/arch 49 | # operator: In 50 | # values: 51 | # - amd64 52 | # - arm64 53 | # - ppc64le 54 | # - s390x 55 | # - key: kubernetes.io/os 56 | # operator: In 57 | # values: 58 | # - linux 59 | securityContext: 60 | runAsNonRoot: true 61 | # TODO(user): For common cases that do not require escalating privileges 62 | # it is recommended to ensure that all your Pods/Containers are restrictive. 63 | # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 64 | # Please uncomment the following code if your project does NOT have to work on old Kubernetes 65 | # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). 66 | # seccompProfile: 67 | # type: RuntimeDefault 68 | containers: 69 | - command: 70 | - /manager 71 | args: 72 | - --leader-elect 73 | image: controller:latest 74 | imagePullPolicy: IfNotPresent 75 | name: manager 76 | securityContext: 77 | allowPrivilegeEscalation: false 78 | capabilities: 79 | drop: 80 | - "ALL" 81 | livenessProbe: 82 | httpGet: 83 | path: /healthz 84 | port: 8081 85 | initialDelaySeconds: 15 86 | periodSeconds: 20 87 | readinessProbe: 88 | httpGet: 89 | path: /readyz 90 | port: 8081 91 | initialDelaySeconds: 5 92 | periodSeconds: 10 93 | # TODO(user): Configure the resources accordingly based on the project requirements. 94 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 95 | resources: 96 | limits: 97 | cpu: 500m 98 | memory: 128Mi 99 | requests: 100 | cpu: 10m 101 | memory: 64Mi 102 | serviceAccountName: controller-manager 103 | terminationGracePeriodSeconds: 10 104 | -------------------------------------------------------------------------------- /config/mysql/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: default # this is necessary for skaffold's healthcheck 2 | resources: 3 | - mysql-deployment.yaml 4 | - mysql-service.yaml 5 | - mysql-service-nodeport.yaml 6 | # - mysql-secret.yaml 7 | -------------------------------------------------------------------------------- /config/mysql/mysql-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app: mysql 7 | name: mysql 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: mysql 13 | strategy: {} 14 | template: 15 | metadata: 16 | creationTimestamp: null 17 | labels: 18 | app: mysql 19 | spec: 20 | containers: 21 | - image: mysql:8 22 | name: mysql 23 | # https://hub.docker.com/_/mysql 24 | env: 25 | - name: MYSQL_ROOT_PASSWORD 26 | value: password 27 | readinessProbe: 28 | tcpSocket: 29 | port: 3306 30 | initialDelaySeconds: 5 31 | periodSeconds: 10 32 | -------------------------------------------------------------------------------- /config/mysql/mysql-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | key: cm9vdA== 4 | kind: Secret 5 | metadata: 6 | name: mysql-user 7 | type: Opaque 8 | --- 9 | apiVersion: v1 10 | data: 11 | key: cGFzc3dvcmQ= 12 | kind: Secret 13 | metadata: 14 | name: mysql-password 15 | type: Opaque 16 | -------------------------------------------------------------------------------- /config/mysql/mysql-service-nodeport.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: mysql 6 | name: mysql-nodeport 7 | spec: 8 | ports: 9 | - name: "3306" 10 | port: 3306 11 | protocol: TCP 12 | nodePort: 30306 13 | selector: 14 | app: mysql 15 | type: NodePort 16 | -------------------------------------------------------------------------------- /config/mysql/mysql-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app: mysql 7 | name: mysql 8 | spec: 9 | ports: 10 | - name: "3306" 11 | port: 3306 12 | protocol: TCP 13 | targetPort: 3306 14 | selector: 15 | app: mysql 16 | type: ClusterIP 17 | status: 18 | loadBalancer: {} 19 | -------------------------------------------------------------------------------- /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 | app.kubernetes.io/name: servicemonitor 9 | app.kubernetes.io/instance: controller-manager-metrics-monitor 10 | app.kubernetes.io/component: metrics 11 | app.kubernetes.io/created-by: mysql-operator 12 | app.kubernetes.io/part-of: mysql-operator 13 | app.kubernetes.io/managed-by: kustomize 14 | name: controller-manager-metrics-monitor 15 | namespace: system 16 | spec: 17 | endpoints: 18 | - path: /metrics 19 | port: https 20 | scheme: https 21 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 22 | tlsConfig: 23 | insecureSkipVerify: true 24 | selector: 25 | matchLabels: 26 | control-plane: controller-manager 27 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: metrics-reader 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: mysql-operator 9 | app.kubernetes.io/part-of: mysql-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: metrics-reader 12 | rules: 13 | - nonResourceURLs: 14 | - "/metrics" 15 | verbs: 16 | - get 17 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: proxy-role 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: mysql-operator 9 | app.kubernetes.io/part-of: mysql-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-role 12 | rules: 13 | - apiGroups: 14 | - authentication.k8s.io 15 | resources: 16 | - tokenreviews 17 | verbs: 18 | - create 19 | - apiGroups: 20 | - authorization.k8s.io 21 | resources: 22 | - subjectaccessreviews 23 | verbs: 24 | - create 25 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: proxy-rolebinding 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: mysql-operator 9 | app.kubernetes.io/part-of: mysql-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: proxy-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: service 7 | app.kubernetes.io/instance: controller-manager-metrics-service 8 | app.kubernetes.io/component: kube-rbac-proxy 9 | app.kubernetes.io/created-by: mysql-operator 10 | app.kubernetes.io/part-of: mysql-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: controller-manager-metrics-service 13 | namespace: system 14 | spec: 15 | ports: 16 | - name: https 17 | port: 8443 18 | protocol: TCP 19 | targetPort: https 20 | selector: 21 | control-plane: controller-manager 22 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /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 | labels: 6 | app.kubernetes.io/name: role 7 | app.kubernetes.io/instance: leader-election-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: mysql-operator 10 | app.kubernetes.io/part-of: mysql-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: leader-election-role 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - configmaps 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - coordination.k8s.io 28 | resources: 29 | - leases 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | - create 35 | - update 36 | - patch 37 | - delete 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - events 42 | verbs: 43 | - create 44 | - patch 45 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: rolebinding 6 | app.kubernetes.io/instance: leader-election-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: mysql-operator 9 | app.kubernetes.io/part-of: mysql-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: leader-election-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: leader-election-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/mysql_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit mysqls. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: mysql-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: mysql-operator 10 | app.kubernetes.io/part-of: mysql-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: mysql-editor-role 13 | rules: 14 | - apiGroups: 15 | - mysql.nakamasato.com 16 | resources: 17 | - mysqls 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - mysql.nakamasato.com 28 | resources: 29 | - mysqls/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/rbac/mysql_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view mysqls. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: mysql-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: mysql-operator 10 | app.kubernetes.io/part-of: mysql-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: mysql-viewer-role 13 | rules: 14 | - apiGroups: 15 | - mysql.nakamasato.com 16 | resources: 17 | - mysqls 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - mysql.nakamasato.com 24 | resources: 25 | - mysqls/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/mysqldb_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit mysqldbs. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: mysqldb-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: mysql-operator 10 | app.kubernetes.io/part-of: mysql-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: mysqldb-editor-role 13 | rules: 14 | - apiGroups: 15 | - mysql.nakamasato.com 16 | resources: 17 | - mysqldbs 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - mysql.nakamasato.com 28 | resources: 29 | - mysqldbs/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/rbac/mysqldb_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view mysqldbs. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: mysqldb-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: mysql-operator 10 | app.kubernetes.io/part-of: mysql-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: mysqldb-viewer-role 13 | rules: 14 | - apiGroups: 15 | - mysql.nakamasato.com 16 | resources: 17 | - mysqldbs 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - mysql.nakamasato.com 24 | resources: 25 | - mysqldbs/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/mysqluser_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit mysqlusers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: mysqluser-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: mysql-operator 10 | app.kubernetes.io/part-of: mysql-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: mysqluser-editor-role 13 | rules: 14 | - apiGroups: 15 | - mysql.nakamasato.com 16 | resources: 17 | - mysqlusers 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - mysql.nakamasato.com 28 | resources: 29 | - mysqlusers/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/rbac/mysqluser_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view mysqlusers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: mysqluser-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: mysql-operator 10 | app.kubernetes.io/part-of: mysql-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: mysqluser-viewer-role 13 | rules: 14 | - apiGroups: 15 | - mysql.nakamasato.com 16 | resources: 17 | - mysqlusers 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - mysql.nakamasato.com 24 | resources: 25 | - mysqlusers/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - secrets 11 | verbs: 12 | - create 13 | - get 14 | - list 15 | - watch 16 | - apiGroups: 17 | - "" 18 | resources: 19 | - events 20 | verbs: 21 | - create 22 | - patch 23 | - update 24 | - apiGroups: 25 | - mysql.nakamasato.com 26 | resources: 27 | - mysqldbs 28 | verbs: 29 | - create 30 | - delete 31 | - get 32 | - list 33 | - patch 34 | - update 35 | - watch 36 | - apiGroups: 37 | - mysql.nakamasato.com 38 | resources: 39 | - mysqldbs/finalizers 40 | verbs: 41 | - update 42 | - apiGroups: 43 | - mysql.nakamasato.com 44 | resources: 45 | - mysqldbs/status 46 | verbs: 47 | - get 48 | - patch 49 | - update 50 | - apiGroups: 51 | - mysql.nakamasato.com 52 | resources: 53 | - mysqls 54 | verbs: 55 | - create 56 | - delete 57 | - get 58 | - list 59 | - patch 60 | - update 61 | - watch 62 | - apiGroups: 63 | - mysql.nakamasato.com 64 | resources: 65 | - mysqls/finalizers 66 | verbs: 67 | - update 68 | - apiGroups: 69 | - mysql.nakamasato.com 70 | resources: 71 | - mysqls/status 72 | verbs: 73 | - get 74 | - patch 75 | - update 76 | - apiGroups: 77 | - mysql.nakamasato.com 78 | resources: 79 | - mysqlusers 80 | verbs: 81 | - create 82 | - delete 83 | - get 84 | - list 85 | - patch 86 | - update 87 | - watch 88 | - apiGroups: 89 | - mysql.nakamasato.com 90 | resources: 91 | - mysqlusers/finalizers 92 | verbs: 93 | - update 94 | - apiGroups: 95 | - mysql.nakamasato.com 96 | resources: 97 | - mysqlusers/status 98 | verbs: 99 | - get 100 | - patch 101 | - update 102 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: manager-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: mysql-operator 9 | app.kubernetes.io/part-of: mysql-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: manager-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: manager-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: serviceaccount 6 | app.kubernetes.io/instance: controller-manager-sa 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: mysql-operator 9 | app.kubernetes.io/part-of: mysql-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/sample-migrations/01_create_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE test_table; 2 | -------------------------------------------------------------------------------- /config/sample-migrations/01_create_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE test_table (id int, name varchar(10)); 2 | -------------------------------------------------------------------------------- /config/samples-on-k8s-with-gcp-secretmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - mysql_v1alpha1_mysqluser.yaml 4 | - mysql_v1alpha1_mysql.yaml 5 | - mysql_v1alpha1_mysqldb.yaml 6 | #+kubebuilder:scaffold:manifestskustomizesamples 7 | -------------------------------------------------------------------------------- /config/samples-on-k8s-with-gcp-secretmanager/mysql_v1alpha1_mysql.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mysql.nakamasato.com/v1alpha1 2 | kind: MySQL 3 | metadata: 4 | name: mysql-sample 5 | spec: 6 | host: "127.0.0.1" # auth SQL proxy 7 | adminUser: 8 | name: root 9 | type: raw 10 | adminPassword: # stored in GCP SecretMamanger 11 | name: mysql-password 12 | type: gcp 13 | -------------------------------------------------------------------------------- /config/samples-on-k8s-with-gcp-secretmanager/mysql_v1alpha1_mysqldb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mysql.nakamasato.com/v1alpha1 2 | kind: MySQLDB 3 | metadata: 4 | name: sample-db # this is not a name for MySQL database but just a Kubernetes object name 5 | spec: 6 | dbName: sample_db # this is MySQL database name 7 | mysqlName: mysql-sample 8 | -------------------------------------------------------------------------------- /config/samples-on-k8s-with-gcp-secretmanager/mysql_v1alpha1_mysqluser.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mysql.nakamasato.com/v1alpha1 2 | kind: MySQLUser 3 | metadata: 4 | name: sample-user 5 | spec: 6 | mysqlName: mysql-sample 7 | -------------------------------------------------------------------------------- /config/samples-on-k8s-with-k8s-secret/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - mysql_v1alpha1_mysqluser.yaml 4 | - mysql_v1alpha1_mysql.yaml 5 | - mysql_v1alpha1_mysqldb.yaml 6 | #+kubebuilder:scaffold:manifestskustomizesamples 7 | -------------------------------------------------------------------------------- /config/samples-on-k8s-with-k8s-secret/mysql_v1alpha1_mysql.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mysql.nakamasato.com/v1alpha1 2 | kind: MySQL 3 | metadata: 4 | name: mysql-sample 5 | spec: 6 | host: "mysql.default" 7 | adminUser: 8 | name: mysql-user 9 | type: k8s 10 | adminPassword: # stored in GCP SecretMamanger 11 | name: mysql-password 12 | type: k8s 13 | -------------------------------------------------------------------------------- /config/samples-on-k8s-with-k8s-secret/mysql_v1alpha1_mysqldb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mysql.nakamasato.com/v1alpha1 2 | kind: MySQLDB 3 | metadata: 4 | name: sample-db # this is not a name for MySQL database but just a Kubernetes object name 5 | spec: 6 | dbName: sample_db # this is MySQL database name 7 | mysqlName: mysql-sample 8 | -------------------------------------------------------------------------------- /config/samples-on-k8s-with-k8s-secret/mysql_v1alpha1_mysqluser.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mysql.nakamasato.com/v1alpha1 2 | kind: MySQLUser 3 | metadata: 4 | name: sample-user 5 | spec: 6 | mysqlName: mysql-sample 7 | -------------------------------------------------------------------------------- /config/samples-on-k8s/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - mysql_v1alpha1_mysqluser.yaml 3 | - mysql_v1alpha1_mysql.yaml 4 | - mysql_v1alpha1_mysqldb.yaml 5 | -------------------------------------------------------------------------------- /config/samples-on-k8s/mysql_v1alpha1_mysql.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mysql.nakamasato.com/v1alpha1 2 | kind: MySQL 3 | metadata: 4 | name: mysql-sample 5 | spec: 6 | host: mysql.default # need to include namespace if you use Kubernetes Service as an endpoint. 7 | adminUser: 8 | name: root 9 | type: raw 10 | adminPassword: 11 | name: password 12 | type: raw # you can choose one of gcp or raw 13 | -------------------------------------------------------------------------------- /config/samples-on-k8s/mysql_v1alpha1_mysqldb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mysql.nakamasato.com/v1alpha1 2 | kind: MySQLDB 3 | metadata: 4 | name: sample-db # this is not a name for MySQL database but just a Kubernetes object name 5 | spec: 6 | dbName: sample_db # this is MySQL database name 7 | mysqlName: mysql-sample 8 | -------------------------------------------------------------------------------- /config/samples-on-k8s/mysql_v1alpha1_mysqluser.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mysql.nakamasato.com/v1alpha1 2 | kind: MySQLUser 3 | metadata: 4 | name: sample-user 5 | spec: 6 | mysqlName: mysql-sample 7 | host: '%' 8 | -------------------------------------------------------------------------------- /config/samples-wtih-gcp-secretmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - mysql_v1alpha1_mysqluser.yaml 4 | - mysql_v1alpha1_mysql.yaml 5 | - mysql_v1alpha1_mysqldb.yaml 6 | #+kubebuilder:scaffold:manifestskustomizesamples 7 | -------------------------------------------------------------------------------- /config/samples-wtih-gcp-secretmanager/mysql_v1alpha1_mysql.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mysql.nakamasato.com/v1alpha1 2 | kind: MySQL 3 | metadata: 4 | name: mysql-sample 5 | spec: 6 | host: localhost 7 | adminUser: 8 | name: root 9 | type: raw 10 | adminPassword: # echo -n "password" | gcloud secrets create mysql-password --data-file=- 11 | name: mysql-password 12 | type: gcp 13 | -------------------------------------------------------------------------------- /config/samples-wtih-gcp-secretmanager/mysql_v1alpha1_mysqldb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mysql.nakamasato.com/v1alpha1 2 | kind: MySQLDB 3 | metadata: 4 | name: sample-db # this is not a name for MySQL database but just a Kubernetes object name 5 | spec: 6 | dbName: sample_db # this is MySQL database name 7 | mysqlName: mysql-sample 8 | -------------------------------------------------------------------------------- /config/samples-wtih-gcp-secretmanager/mysql_v1alpha1_mysqluser.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mysql.nakamasato.com/v1alpha1 2 | kind: MySQLUser 3 | metadata: 4 | name: sample-user 5 | spec: 6 | mysqlName: mysql-sample 7 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples of your project ## 2 | resources: 3 | - mysql_v1alpha1_mysql.yaml 4 | - mysql_v1alpha1_mysqldb.yaml 5 | - mysql_v1alpha1_mysqluser.yaml 6 | #+kubebuilder:scaffold:manifestskustomizesamples 7 | -------------------------------------------------------------------------------- /config/samples/mysql_v1alpha1_mysql.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mysql.nakamasato.com/v1alpha1 2 | kind: MySQL 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: mysql 6 | app.kubernetes.io/instance: mysql-sample 7 | app.kubernetes.io/part-of: mysql-operator 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: mysql-operator 10 | name: mysql-sample 11 | spec: 12 | host: localhost 13 | adminUser: 14 | name: root 15 | type: raw 16 | adminPassword: 17 | name: password 18 | type: raw 19 | -------------------------------------------------------------------------------- /config/samples/mysql_v1alpha1_mysqldb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mysql.nakamasato.com/v1alpha1 2 | kind: MySQLDB 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: mysqldb 6 | app.kubernetes.io/instance: mysqldb-sample 7 | app.kubernetes.io/part-of: mysql-operator 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: mysql-operator 10 | name: sample-db # this is not a name for MySQL database but just a Kubernetes object name 11 | spec: 12 | dbName: sample_db # this is MySQL database name 13 | mysqlName: mysql-sample 14 | schemaMigrationFromGitHub: 15 | owner: nakamasato 16 | repo: mysql-operator 17 | path: config/sample-migrations 18 | ref: 96dc1eeaf00c8afb42f1c9b63859ff57c440e584 19 | # "github://nakamasato/mysql-operator/config/sample-migrations#96dc1eeaf00c8afb42f1c9b63859ff57c440e584", // Currently only support GitHub source 20 | -------------------------------------------------------------------------------- /config/samples/mysql_v1alpha1_mysqluser.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mysql.nakamasato.com/v1alpha1 2 | kind: MySQLUser 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: mysqluser 6 | app.kubernetes.io/instance: sample-user 7 | app.kubernetes.io/part-of: mysql-operator 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: mysql-operator 10 | name: sample-user 11 | spec: 12 | mysqlName: mysql-sample 13 | -------------------------------------------------------------------------------- /docs/developer-guide/api-resources.md: -------------------------------------------------------------------------------- 1 | # API resources 2 | 3 | - API resources are defined in `api//xxx_types.go`. 4 | - Manifest file for `CustomResourceDefinition` is generated by `make manifests` (`controller-gen`). 5 | - `DeepCopy` is generated by `make generate` (`controller-gen`). 6 | 7 | ## `MySQL` 8 | 9 | MySQL represents a MySQL cluster with root acess. 10 | 11 | - Spec 12 | - AdminUser 13 | - AdminPassword 14 | - Status 15 | - UserCount 16 | - DBCount 17 | 18 | TODO: 19 | 20 | - [x] Credential management. ([#190 GCP SecretManager](https://github.com/nakamasato/mysql-operator/pull/190)) 21 | - [ ] Change to `ClusterResource` so `MySQLUser` in any namespace can reference it. (No need of changing `OwnerReference`) 22 | 23 | > Namespaced dependents can specify cluster-scoped or namespaced owners. 24 | Ref: [Owner references in object specifications](https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/#owner-references-in-object-specifications) 25 | 26 | ## `MySQLUser` 27 | 28 | When `MySQLUser` is created/edited/deleted, MySQL user will be created/edited/deleted by the controller. 29 | 30 | - Spec 31 | - MysqlName: The name of `MySQL` object 32 | - Host: MySQL user's host 33 | - Status 34 | - Conditions 35 | - Phase: `Ready` if Secret and MySQL user are created, otherwise `NotReady` 36 | - Reason: Reason for `NotReady` 37 | 38 | ## `MySQLDB` 39 | 40 | You can create MySQL database with this custom resource. 41 | 42 | - Spec 43 | - DBName: The database name. (The reason for not directly using the object's name is becase some object name can't be used for database name) 44 | - MysqlName: The name of `MySQL` object 45 | 46 | ToDo: 47 | 48 | - [ ] Validate `DBName` 49 | -------------------------------------------------------------------------------- /docs/developer-guide/helm.md: -------------------------------------------------------------------------------- 1 | # helm 2 | 3 | ## Create Helm chart 4 | 5 | With [helmify](https://github.com/arttor/helmify), you can create a helm chart 6 | 7 | 1. Update Makefile 8 | ``` 9 | HELMIFY ?= $(LOCALBIN)/helmify 10 | 11 | .PHONY: helmify 12 | helmify: $(HELMIFY) ## Download helmify locally if necessary. 13 | $(HELMIFY): $(LOCALBIN) 14 | test -s $(LOCALBIN)/helmify || GOBIN=$(LOCALBIN) go install github.com/arttor/helmify/cmd/helmify@latest 15 | 16 | helm: manifests kustomize helmify 17 | $(KUSTOMIZE) build config/install | $(HELMIFY) 18 | ``` 19 | 1. Run 20 | ``` 21 | make helm 22 | ``` 23 | 1. Check generated files 24 | ``` 25 | chart 26 | ├── Chart.yaml 27 | ├── templates 28 | │ ├── _helpers.tpl 29 | │ ├── deployment.yaml 30 | │ ├── leader-election-rbac.yaml 31 | │ ├── manager-config.yaml 32 | │ ├── manager-rbac.yaml 33 | │ ├── metrics-reader-rbac.yaml 34 | │ ├── metrics-service.yaml 35 | │ ├── mysql-crd.yaml 36 | │ ├── mysqldb-crd.yaml 37 | │ ├── mysqluser-crd.yaml 38 | │ └── proxy-rbac.yaml 39 | └── values.yaml 40 | 41 | 1 directory, 13 files 42 | ``` 43 | 1. Update name in `chart/Chart.yaml` 44 | ```yaml 45 | name: mysql-operator 46 | ``` 47 | 1. Update `chart/templates/deployment.yaml` for your purpose 48 | What we do here is basically to enable to change `Deployment` from `Values`. (ref: [#199](https://github.com/nakamasato/mysql-operator/pull/199/commits/cc245343a9a24eee35425ef7d665c9d17996c7a8)) 49 | 1. Package 50 | ``` 51 | helm package chart --app-version v0.2.0 52 | ``` 53 | The command will generate `mysql-operator-0.1.0.tgz` 54 | ## Publish package to Helm chart repo. 55 | 56 | https://github.com/nakamasato/helm-charts is used for repo. 57 | All we need to do is to update the chart source file under [charts/mysql-operator](https://github.com/nakamasato/helm-charts/tree/main/charts/mysql-operator) in the repo. 58 | 59 | We use GitHub Actions to update the repo. 60 | 61 | ## Install mysql-operator with the Helm chart (from local source file) 62 | 63 | 1. Install mysql-operator with helm 64 | 65 | ``` 66 | helm install mysql-operator-0.1.0.tgz --generate-name 67 | ``` 68 | 69 | Optionally, you can add `--set adminUserSecretType=gcp --set gcpProjectId=$PROJECT_ID` to use GCP SecretManager to get AdminUser and/or AdminPassword. 70 | 71 |
72 | 73 | ``` 74 | NAME: mysql-operator-0-1680907162 75 | LAST DEPLOYED: Sat Apr 8 07:13:58 2023 76 | NAMESPACE: default 77 | STATUS: deployed 78 | REVISION: 1 79 | TEST SUITE: None 80 | ``` 81 | 82 |
83 | 84 | 1. List 85 | 86 | ``` 87 | helm list 88 | ``` 89 | 90 | ``` 91 | NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION 92 | mysql-operator-0-1680907162 default 1 2023-04-08 07:39:22.416055 +0900 JST deployed mysql-operator-0.1.0 v0.2.0 93 | ``` 94 | 1. Check operator is running 95 | ``` 96 | kubectl get po 97 | NAME READY STATUS RESTARTS AGE 98 | mysql-operator-0-1680907162-controller-manager-f9d855dc9-d4psm 0/1 Running 0 13s 99 | ``` 100 | 1. (Optional) upgrade an existing release 101 | ``` 102 | helm upgrade mysql-operator-0-1680913123 $HELM_PATH --set adminUserSecretType=gcp --set gcpProjectId=$PROJECT_ID 103 | ``` 104 | 1. Uninstall 105 | ``` 106 | helm uninstall mysql-operator-0-1680907162 107 | ``` 108 | 109 | ## Usage 110 | 111 | [Install with Helm](../usage/install-with-helm.md) 112 | 113 | ## Development Tips 114 | 115 | 1. Check resulting yaml file 116 | ``` 117 | helm template chart 118 | ``` 119 | -------------------------------------------------------------------------------- /docs/developer-guide/reconciliation.md: -------------------------------------------------------------------------------- 1 | # Reconciliation Loop (Old) 2 | 3 | ![](reconciliation.drawio.svg) 4 | 5 | ## 1. Update subresource `status.conditions` (with operator-utils) 6 | 7 | Use https://github.com/redhat-cop/operator-utils 8 | 9 | Usage: 10 | 11 | 1. Update Reconciler with: 12 | ```go 13 | import "github.com/redhat-cop/operator-utils/pkg/util" 14 | 15 | ... 16 | type MyReconciler struct { 17 | util.ReconcilerBase 18 | Log logr.Logger 19 | ... other optional fields ... 20 | } 21 | ``` 22 | 23 | 1. Update CRD: 24 | ```go 25 | // +patchMergeKey=type 26 | // +patchStrategy=merge 27 | // +listType=map 28 | // +listMapKey=type 29 | Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` 30 | } 31 | 32 | func (m *MyCRD) GetConditions() []metav1.Condition { 33 | return m.Status.Conditions 34 | } 35 | 36 | func (m *MyCRD) SetConditions(conditions []metav1.Condition) { 37 | m.Status.Conditions = conditions 38 | } 39 | ``` 40 | 41 | 1. Replace `return ctrl.Result{}, err` with: 42 | 43 | ```go 44 | return r.ManageError(ctx, instance, err) 45 | ``` 46 | 47 | 1. Replace `return ctrl.Result{}, nil` with: 48 | 49 | ```go 50 | return r.ManageSuccess(ctx, instance) 51 | ``` 52 | 1. Object will have conditions: 53 | ``` 54 | kubectl get mysqluser -o yaml 55 | ``` 56 | ```yaml 57 | status: 58 | conditions: 59 | - lastTransitionTime: "2021-12-28T12:26:21Z" 60 | message: "" 61 | observedGeneration: 1 62 | reason: LastReconcileCycleSucceded 63 | status: "True" 64 | type: ReconcileSuccess 65 | ``` 66 | 67 | ## 2. Update subresource `status` 68 | 69 | 1. Set mysqluser instance. 70 | 71 | ```go 72 | mysqlUser.Status.Phase = "NotReady" 73 | mysqlUser.Status.Reason = msg 74 | ``` 75 | 1. Save the instance. 76 | 77 | ``` 78 | err := r.Status.Update(ctx, mysqlUser) 79 | ``` 80 | 81 | > When updating the status subresource from the client, the StatusWriter must be used. The status subresource is retrieved with Status() and updated with Update() or patched with Patch(). 82 | 83 | https://sdk.operatorframework.io/docs/building-operators/golang/references/client/#updating-status-subresource 84 | 85 | ## 3. Display field in the result of `kubectl get` (`additionalPrinterColumns`) 86 | 87 | If you don't want to `kubebuilder` marker, you can write CRD by yourself. 88 | 89 | 1. Add a column to Status. 90 | ```diff 91 | type MySQLStatus struct { 92 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 93 | // Important: Run "make" to regenerate code after modifying this file 94 | + 95 | + //+kubebuilder:default=0 96 | + UserCount int32 `json:"userCount"` 97 | } 98 | ``` 99 | 1. Add kubebuilder marker. 100 | ```diff 101 | //+kubebuilder:object:root=true 102 | //+kubebuilder:subresource:status 103 | +//+kubebuilder:printcolumn:name="UserCount",type="integer",JSONPath=".status.userCount",description="The number of MySQLUsers that belongs to the MySQL" 104 | 105 | // MySQL is the Schema for the mysqls API 106 | type MySQL struct { 107 | ``` 108 | 1. Run `make manifests`. 109 | 110 | ```diff 111 | singular: mysql 112 | scope: Namespaced 113 | versions: 114 | - - name: v1alpha1 115 | + - additionalPrinterColumns: 116 | + - description: The number of MySQLUsers that belongs to the MySQL 117 | + jsonPath: .status.userCount 118 | + name: UserCount 119 | + type: integer 120 | + name: v1alpha1 121 | schema: 122 | openAPIV3Schema: 123 | description: MySQL is the Schema for the mysqls API 124 | @@ -52,6 +57,13 @@ spec: 125 | type: object 126 | status: 127 | description: MySQLStatus defines the observed state of MySQL 128 | + properties: 129 | + userCount: 130 | + default: 0 131 | + format: int32 132 | + type: integer 133 | + required: 134 | + - userCount 135 | type: object 136 | type: object 137 | served: true 138 | ``` 139 | 140 | 1. `kubectl get` 141 | ``` 142 | kubectl get mysql 143 | NAME USERCOUNT 144 | mysql-sample 145 | ``` 146 | 147 | https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#additional-printer-columns 148 | ## 4. Add `OwnerReference` or `SetControllerReference` between CustomResources 149 | 150 | 1. Add `SetControllerReference` for MySQL 151 | ```go 152 | controllerutil.SetControllerReference(mysql, mysqlUser, r.Scheme) 153 | err := r.GetClient().Update(ctx, mysqlUser) 154 | if err != nil { 155 | return r.ManageError(ctx, mysqlUser, err) // requeue 156 | } 157 | ``` 158 | 159 | 1. Get in yaml format. 160 | 161 | ```yaml 162 | kubectl get mysqluser nakamasato -o yaml 163 | ... 164 | metadata: 165 | ... 166 | ownerReferences: 167 | - apiVersion: mysql.nakamasato.com/v1alpha1 168 | blockOwnerDeletion: true 169 | controller: true 170 | kind: MySQL 171 | name: mysql-sample 172 | uid: 0689bf66-86a3-40a5-8e50-5e91533a8dc8 173 | resourceVersion: "928" 174 | uid: 09c69b78-79c5-4af8-9f84-7eb5dba52371 175 | ... 176 | ``` 177 | 178 | - [SetControllerReference](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/controller/controllerutil#SetControllerReference): Use this when you want to reconcile the owner object on changes to controlled one. 179 | > SetControllerReference sets owner as a Controller OwnerReference on controlled. This is used for garbage collection of the controlled object and for reconciling the owner object on changes to controlled (with a Watch + EnqueueRequestForOwner). 180 | 181 | Usually use with the following line in `SetupWithManager`: 182 | ```go 183 | Owns(&mysqlv1alpha1.MySQLUser{}). 184 | ``` 185 | - [SetOwnerReference](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/controller/controllerutil#SetOwnerReference): Use this when you just want garbage collection. 186 | > SetOwnerReference is a helper method to make sure the given object contains an object reference to the object provided. This allows you to declare that owner has a dependency on the object without specifying it as a controller. 187 | 188 | ## 5. Finalizer (Handle Cleanup on Deletion of external resource) 189 | 190 | **Finalizer** is set to wait until dependents are deleted before deleting the object. 191 | 192 | 1. When a new object is created, add the finalizer. 193 | 1. When an object is deleted, `DeletionTimestamp` will be set. 194 | 1. Execute the finalizer logic if the finalizer exists. 195 | 1. Remove the finalizer. 196 | > Once the list of finalizers is empty, meaning all finalizers have been executed, the resource is deleted by Kubernetes. 197 | 198 | - https://kubernetes.io/blog/2021/05/14/using-finalizers-to-control-deletion/ 199 | - https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#advanced-topics 200 | - https://book.kubebuilder.io/reference/using-finalizers.html 201 | - https://sdk.operatorframework.io/docs/building-operators/golang/advanced-topics/#handle-cleanup-on-deletion 202 | -------------------------------------------------------------------------------- /docs/developer-guide/tools.md: -------------------------------------------------------------------------------- 1 | # Tools 2 | 3 | 1. [operator-sdk](https://sdk.operatorframework.io/): Kubernetes operator framework 4 | 1. [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime): Kubernetes operator development tool 5 | 1. [controller-tools](https://github.com/kubernetes-sigs/controller-tools): 6 | 1. bump to v0.13.0 in controller-runtime: https://github.com/kubernetes-sigs/controller-runtime/pull/2450 7 | 1. [golang-migrate](https://github.com/golang-migrate/migrate): database schema migration tool 8 | 1. [atlas](https://atlasgo.io/): database schema migration tool 9 | 1. [helm](https://helm.sh/): release package management 10 | 1. [kind](https://kind.sigs.k8s.io/): local cluster 11 | 1. [kuttl](https://kuttl.dev/): e2e test 12 | 1. [ginkgo](https://github.com/onsi/ginkgo): Go test 13 | 1. [skaffold](https://skaffold.dev/): local run & mysql-operator setup for testing 14 | -------------------------------------------------------------------------------- /docs/developer-guide/versions.md: -------------------------------------------------------------------------------- 1 | # Versions 2 | 3 | ## operator-sdk 4 | 5 | Originally created with [v1.10.1](https://github.com/operator-framework/operator-sdk/releases/tag/v1.10.1) 6 | 7 | `Makefile` was updated with [v1.28.0](https://github.com/operator-framework/operator-sdk/releases/tag/v1.28.0) 8 | 9 | Steps: 10 | 11 | 1. create temporary dir 12 | 1. create operator-sdk project 13 | ``` 14 | operator-sdk init --domain nakamasato.com --repo github.com/nakamasato/mysql-operator 15 | ``` 16 | 1. copy `Makefile` to this repo 17 | 1. Update a few points 18 | 1. IMAGE_TAG_BASE 19 | ``` 20 | IMAGE_TAG_BASE ?= nakamasato/mysql-operator 21 | ``` 22 | 1. IMG 23 | ``` 24 | IMG ?= ghcr.io/nakamasato/mysql-operator 25 | ``` 26 | 1. test 27 | ``` 28 | test: manifests generate fmt vet envtest ## Run tests. 29 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" $(GINKGO) -cover -coverprofile cover.out -covermode=atomic -sk 30 | ip-package=e2e ./... 31 | ``` 32 | 1. gingko 33 | ``` 34 | GINKGO_VERSION ?= v2.9.2 35 | ``` 36 | 37 | ``` 38 | GINKGO = $(LOCALBIN)/ginkgo 39 | ginkgo: 40 | test -s $(LOCALBIN)/ginkgo && $(LOCALBIN)/ginkgo version | grep -q $(GINKGO_VERSION) || \ 41 | GOBIN=$(LOCALBIN) go install github.com/onsi/ginkgo/v2/ginkgo@$(GINKGO_VERSION) 42 | ``` 43 | 1. helmify 44 | 45 | ``` 46 | HELMIFY ?= $(LOCALBIN)/helmify 47 | 48 | .PHONY: helmify 49 | helmify: $(HELMIFY) ## Download helmify locally if necessary. 50 | $(HELMIFY): $(LOCALBIN) 51 | test -s $(LOCALBIN)/helmify || GOBIN=$(LOCALBIN) go install github.com/arttor/helmify/cmd/helmify@latest 52 | 53 | helm: manifests kustomize helmify 54 | $(KUSTOMIZE) build config/default | $(HELMIFY) 55 | ``` 56 | 57 | ## kubebuilder 58 | 59 | ### Migration from go/v3 to go/v4 60 | 61 | - https://book.kubebuilder.io/migration/manually_migration_guide_gov3_to_gov4 62 | - https://github.com/kubernetes-sigs/kubebuilder/blob/master/testdata/project-v4/Makefile 63 | - https://book.kubebuilder.io/migration/migration_guide_gov3_to_gov4 64 | 65 | 66 | ``` 67 | kubebuilder version 68 | Version: main.version{KubeBuilderVersion:"3.11.0", KubernetesVendor:"1.27.1", GitCommit:"3a3d1d9573f5b8fe7252bf49cec6e67ba87c88e7", BuildDate:"2023-06-20T19:20:03Z", GoOs:"darwin", GoArch:"arm64"} 69 | ``` 70 | 71 | ``` 72 | go mod init github.com/nakamasato/mysql-operator 73 | ``` 74 | 75 | ``` 76 | kubebuilder init --domain nakamasato.com --plugins=go/v4 77 | ``` 78 | 79 | ``` 80 | kubebuilder create api --group mysql --version v1alpha1 --kind MySQL --controller --api 81 | kubebuilder create api --group mysql --version v1alpha1 --kind MySQLDB --controller --api 82 | kubebuilder create api --group mysql --version v1alpha1 --kind MySQLUser --controller --api 83 | ``` 84 | 85 | Copy apis 86 | 87 | Copy internal packages 88 | ``` 89 | cp -r ../mysql-operator/internal/metrics internal 90 | cp -r ../mysql-operator/internal/mysql internal/mysql 91 | cp -r ../mysql-operator/internal/secret internal/ 92 | cp -r ../mysql-operator/internal/utils internal/ 93 | ``` 94 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This is a go-based Kubernetes operator built with [operator-sdk](https://sdk.operatorframework.io/docs/building-operators/golang/), which manages MySQL databases, schema, users, permissions in existing MySQL servers. This operator DOES NOT manage MySQL cluster like other MySQL operators such as [vitess](https://github.com/vitessio/vitess), [mysql/mysql-operator](https://github.com/mysql/mysql-operator). 4 | 5 | ## Motivation 6 | 7 | Reduce human operations: 8 | 9 | 1. **User management**: When creating a MySQL user for an application running on Kubernetes, it's necessary to create a MySQL user and create a Secret manually or with a script, which can be replaced with a Kubernetes operator. The initial idea is from KafkaUser and KafkaTopic in [Strimzi Kafka Operator](https://github.com/strimzi/strimzi-kafka-operator). With a custom resource for MySQL user, we can manage MySQL users with Kubernetes manifest files as a part of dependent application. 10 | Benefits from such a custom resource and operator: 11 | 1. Kubernetes manifest files for an application and its dependent resources (including MySQL user) can be managed together with Kustomize or Helm chart, with which we can easily duplicate whole environment. 12 | 1. There's no chance to require someone to check the raw password as it's stored directly to Secret by the operator, and read by the dependent application from the Secret. 13 | 1. **Database migration**: Reduce manual operations but keep changelog. When any schema migration or database operation is required, we needed a human operation, which has potential risk of human errors that should be avoided. With a Kubernetes operator, we can execute each database operation in the standard way with traceable changlog. 14 | 15 | ## Custom Resources 16 | * `MySQL` - MySQL cluster or server. 17 | * `MySQLUser` - MySQL user. 18 | * `MySQLDB` - MySQL database. 19 | 20 | ## Contents 21 | 22 | - Developer Guide 23 | - [Reconciliation](developer-guide/reconciliation.md) 24 | - [API Resource](developer-guide/api-resources.md) 25 | - [Debug](developer-guide/debug.md) 26 | - [Helm](developer-guide/helm.md) 27 | - [Testing](developer-guide/testing.md) 28 | - [Tools](developer-guide/tools.md) 29 | - Usage 30 | - [Run on GKE and manage Cloud SQL (MySQL) with GCP SecretManager](usage/gcp-secretmanager.md) 31 | - [Schema Migration](usage/schema-migration.md) 32 | - [Install with Helm](usage/install-with-helm.md) 33 | 34 | 35 | ## Getting Started 36 | 37 | 1. Install CRD 38 | ``` 39 | kubectl apply -k https://github.com/nakamasato/mysql-operator/config/install 40 | ``` 41 | 1. (Optional) prepare MySQL. 42 | ``` 43 | kubectl apply -k https://github.com/nakamasato/mysql-operator/config/mysql 44 | ``` 45 | 46 | 1. Configure MySQL credentials for the operator using the custom resources `MySQL`. 47 | 48 | `mysql.yaml` credentials to connect to the MySQL: **This user is used to manage MySQL users and databases, which is ususally an admin user.** 49 | 50 | ```yaml 51 | apiVersion: mysql.nakamasato.com/v1alpha1 52 | kind: MySQL 53 | metadata: 54 | name: mysql-sample 55 | spec: 56 | host: mysql.default # need to include namespace if you use Kubernetes Service as an endpoint. 57 | adminUser: 58 | name: root 59 | type: raw 60 | adminPassword: 61 | name: password 62 | type: raw 63 | ``` 64 | 65 | If you installed mysql sample with the command above, the password for the root user is `password`. You can apply `MySQL` with the following command. 66 | 67 | ``` 68 | kubectl apply -f https://raw.githubusercontent.com/nakamasato/mysql-operator/main/config/samples-on-k8s/mysql_v1alpha1_mysql.yaml 69 | ``` 70 | 71 | You can check the `MySQL` object and status: 72 | 73 | ``` 74 | kubectl get mysql 75 | NAME HOST ADMINUSER CONNECTED USERCOUNT DBCOUNT REASON 76 | mysql-sample mysql.default root true 0 0 Ping succeded and updated MySQLClients 77 | ``` 78 | 79 | 1. Create a new MySQL user with custom resource `MySQLUser`. 80 | 81 | `mysqluser.yaml`: MySQL user 82 | 83 | ```yaml 84 | apiVersion: mysql.nakamasato.com/v1alpha1 85 | kind: MySQLUser 86 | metadata: 87 | name: sample-user 88 | spec: 89 | mysqlName: mysql-sample 90 | host: '%' 91 | ``` 92 | 93 | 1. Create a new MySQL user `sample-user` 94 | 95 | ``` 96 | kubectl apply -f https://raw.githubusercontent.com/nakamasato/mysql-operator/main/config/samples-on-k8s/mysql_v1alpha1_mysqluser.yaml 97 | ``` 98 | 99 | 1. You can check the status of `MySQLUser` object 100 | 101 | ``` 102 | kubectl get mysqluser 103 | NAME MYSQLUSER SECRET PHASE REASON 104 | sample-user true true Ready Both secret and mysql user are successfully created. 105 | ``` 106 | 107 | 1. You can also confirm the Secret for the new MySQL user is created. 108 | 109 | ``` 110 | kubectl get secret 111 | NAME TYPE DATA AGE 112 | mysql-mysql-sample-sample-user Opaque 1 4m3s 113 | ``` 114 | 115 | 1. Connect to MySQL with the newly created user 116 | 117 | ``` 118 | kubectl exec -it $(kubectl get po | grep mysql | head -1 | awk '{print $1}') -- mysql -usample-user -p$(kubectl get secret mysql-mysql-sample-sample-user -o jsonpath='{.data.password}' | base64 --decode) 119 | ``` 120 | 121 | 1. Create a new MySQL database with custom resource `MySQLDB`. 122 | 123 | `mysqldb.yaml`: MySQL database 124 | 125 | ```yaml 126 | apiVersion: mysql.nakamasato.com/v1alpha1 127 | kind: MySQLDB 128 | metadata: 129 | name: sample-db # this is not a name for MySQL database but just a Kubernetes object name 130 | spec: 131 | dbName: sample_db # this is MySQL database name 132 | mysqlName: mysql-sample 133 | ``` 134 | 135 | ``` 136 | kubectl apply -f https://raw.githubusercontent.com/nakamasato/mysql-operator/main/config/samples-on-k8s/mysql_v1alpha1_mysqldb.yaml 137 | ``` 138 | 139 | ``` 140 | kubectl get mysqldb 141 | NAME PHASE REASON SCHEMAMIGRATION 142 | sample-db Ready Database successfully created {"dirty":false,"version":0} 143 | ``` 144 | 145 | 1. Grant all priviledges of the created db (`sample_db`) to the create user (`sample-user`) (TODO: Currently there's no way to manage user permissions with operator.) 146 | 147 | ``` 148 | kubectl exec -it $(kubectl get po | grep mysql | head -1 | awk '{print $1}') -- mysql -uroot -ppassword 149 | ``` 150 | 151 | ```sql 152 | GRANT ALL PRIVILEGES ON sample_db.* TO 'sample-user'@'%'; 153 | ``` 154 | 155 | Now the created user got the permission to use `sample_db`. 156 | 157 | ``` 158 | ubectl exec -it $(kubectl get po | grep mysql | head -1 | awk '{print $1}') -- mysql -usample-user -p$(kubectl get secret mysql-mysql-sample-sample-user -o jsonpath='{.data.password}' | base64 --decode) 159 | ``` 160 | 161 | ``` 162 | mysql> show databases; 163 | +--------------------+ 164 | | Database | 165 | +--------------------+ 166 | | information_schema | 167 | | performance_schema | 168 | | sample_db | 169 | +--------------------+ 170 | 3 rows in set (0.00 sec) 171 | ``` 172 | 173 | 1. Delete custom resources (`MySQL`, `MySQLUser`, `MySQLDB`). 174 | Example: 175 | ``` 176 | kubectl delete -k https://github.com/nakamasato/mysql-operator/config/samples-on-k8s 177 | ``` 178 | 179 |
NOTICE 180 | 181 | custom resources might get stuck if MySQL is deleted before (to be improved). → Remove finalizers to forcifully delete the stuck objects: 182 | ``` 183 | kubectl patch mysqluser -p '{"metadata":{"finalizers": []}}' --type=merge 184 | ``` 185 | ``` 186 | kubectl patch mysql -p '{"metadata":{"finalizers": []}}' --type=merge 187 | ``` 188 | 189 | ``` 190 | kubectl patch mysqldb -p '{"metadata":{"finalizers": []}}' --type=merge 191 | ``` 192 | 193 |
194 | 195 | 1. (Optional) Delete MySQL 196 | ``` 197 | kubectl delete -k https://github.com/nakamasato/mysql-operator/config/mysql 198 | ``` 199 | 1. Uninstall `mysql-operator` 200 | ``` 201 | kubectl delete -k https://github.com/nakamasato/mysql-operator/config/install 202 | ``` 203 | -------------------------------------------------------------------------------- /docs/prometheus-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakamasato/mysql-operator/70cc16f9e77c5ab109ba4573c6e8c2dbf3b6ee2c/docs/prometheus-graph.png -------------------------------------------------------------------------------- /docs/prometheus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nakamasato/mysql-operator/70cc16f9e77c5ab109ba4573c6e8c2dbf3b6ee2c/docs/prometheus.png -------------------------------------------------------------------------------- /docs/usage/gcp-secretmanager.md: -------------------------------------------------------------------------------- 1 | # Run on GKE and manage Cloud SQL (MySQL) with GCP SecretManager 2 | 3 | mysql-operator can get the credentials of the `MySQL` user (which is used to access to the target MySQL cluster) from [GCP SecretManager](https://cloud.google.com/secret-manager) 4 | 5 | In this example, we'll use Cloud SQL for MySQL, and run mysql-operator on GKE. 6 | 7 | ## 1. Prepare GCP resources 8 | 9 | ### 1.1. Prepare env var and gcloud 10 | 11 | 1. Set environment variables 12 | ``` 13 | INSTANCE_NAME=mysql-test 14 | ZONE=asia-northeast1-b 15 | REGION=asia-northeast1 16 | SECRET_NAME=mysql-password 17 | SA_NAME=mysql-operator 18 | GKE_CLUSTER_NAME=hello-cluster 19 | NAMESPACE=mysql-operator 20 | KSA_NAME=mysql-operator-controller-manager 21 | ``` 22 | 23 | 1. Configure gcloud project 24 | 25 | ``` 26 | PROJECT= 27 | gcloud config set project $PROJECT 28 | ``` 29 | 30 | 31 | ### 1.2. Create GKE cluster 32 | 33 | 1. Create GKE cluster 34 | 35 | ``` 36 | gcloud container clusters create-auto $GKE_CLUSTER_NAME --location=$REGION 37 | ``` 38 | 1. Set up kubeconfig 39 | ``` 40 | gcloud container clusters get-credentials $GKE_CLUSTER_NAME --location=$REGION 41 | ``` 42 | 43 | ### 1.3. Create Cloud SQL instance. 44 | 45 | 1. Generate random password for root user. 46 | ``` 47 | ROOT_PASSWORD=$(openssl rand -base64 32) 48 | ``` 49 | 1. Create Cloud SQL instance. 50 | ``` 51 | gcloud sql instances create $INSTANCE_NAME \ 52 | --cpu=1 \ 53 | --memory=3840MiB \ 54 | --zone=${ZONE} \ 55 | --root-password=$ROOT_PASSWORD \ 56 | --project ${PROJECT} 57 | ``` 58 | 59 | For existing instance, you can reset the root password with the following command: 60 | 61 | ``` 62 | gcloud sql users set-password root \ 63 | --host=% \ 64 | --instance=$INSTANCE_NAME \ 65 | --password=$ROOT_PASSWORD 66 | ``` 67 | 68 | ### 1.4. Create SecretManager secret for root password 69 | 70 | 1. Create Secret `mysql-password` with value `password`, which will be used for the credentials of custom resource `MySQL`. 71 | ``` 72 | gcloud secrets create $SECRET_NAME --replication-policy="automatic" --project ${PROJECT} 73 | ``` 74 | 75 | ``` 76 | echo -n "${ROOT_PASSWORD}" | gcloud secrets versions add $SECRET_NAME --data-file=- --project ${PROJECT} 77 | ``` 78 | ### 1.5. Create Service Account for `mysql-operator` 79 | 80 | 1. Create service account `mysql-operator` 81 | 82 | ``` 83 | gcloud iam service-accounts create $SA_NAME --display-name=$SA_NAME 84 | ``` 85 | 86 | 1. Grant necessary permission for the created `Secret` to the service account 87 | 88 | 1. `roles/secretmanager.secretAccessor`: To allow mysql-operator to get root password from SecretManager 89 | 90 | ``` 91 | gcloud secrets add-iam-policy-binding $SECRET_NAME \ 92 | --member="serviceAccount:${SA_NAME}@${PROJECT}.iam.gserviceaccount.com" \ 93 | --role="roles/secretmanager.secretAccessor" --project ${PROJECT} 94 | ``` 95 | 96 | 1. `roles/cloudsql.client`: To allow mysql-operator can connect to Cloud SQL 97 | ``` 98 | gcloud projects add-iam-policy-binding $PROJECT \ 99 | --member="serviceAccount:${SA_NAME}@${PROJECT}.iam.gserviceaccount.com" \ 100 | --role="roles/cloudsql.client" 101 | ``` 102 | 103 | 1. `roles/iam.workloadIdentityUser`: To allow to Kubernete Pod to impersonate the Service Account 104 | 105 | ``` 106 | gcloud iam service-accounts add-iam-policy-binding ${SA_NAME}@${PROJECT}.iam.gserviceaccount.com \ 107 | --role roles/iam.workloadIdentityUser \ 108 | --member "serviceAccount:${PROJECT}.svc.id.goog[${NAMESPACE}/${KSA_NAME}]" 109 | ``` 110 | 111 | For more details, read [Workload Identity](https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity) 112 | 113 | ## 2. Install `mysql-operator` with Helm 114 | 115 | 1. Create a Namespace. 116 | 117 | ``` 118 | kubectl create ns $NAMESPACE 119 | ``` 120 | 121 | 1. Deploy with Helm. 122 | 123 | ``` 124 | helm repo add nakamasato https://nakamasato.github.io/helm-charts 125 | helm repo update 126 | ``` 127 | 128 | ``` 129 | helm install mysql-operator nakamasato/mysql-operator \ 130 | --set adminUserSecretType=gcp \ 131 | --set gcpServiceAccount=${SA_NAME}@${PROJECT}.iam.gserviceaccount.com \ 132 | --set gcpProjectId=$PROJECT \ 133 | --set cloudSQL.instanceConnectionName=$PROJECT:$REGION:$INSTANCE_NAME \ 134 | -n $NAMESPACE 135 | ``` 136 | 137 | 1. Check Helm release. 138 | 139 | ``` 140 | helm list -n $NAMESPACE 141 | NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION 142 | mysql-operator mysql-operator 1 2023-09-09 12:03:54.220046 +0900 JST deployed mysql-operator-v0.3.0 v0.3.0 143 | ``` 144 | 1. Check `mysql-operator` Pod. 145 | ``` 146 | kubectl get pod -n $NAMESPACE 147 | NAME READY STATUS RESTARTS AGE 148 | mysql-operator-controller-manager-77649f6bb9-xbt9l 2/2 Running 0 2m59s 149 | ``` 150 | 151 | ## 3. Create custom resources (Manage MySQL users, databases, schemas, etc.) 152 | 153 | 1. Create sample `MySQL`, `MySQLUser`, `MySQLDB`. 154 | 155 | If you want to create sample MySQL, MySQLUser, and `MySQLDB` at once, you can use the following command: 156 | 157 | ``` 158 | kubectl apply -k https://github.com/nakamasato/mysql-operator/config/samples-on-k8s-with-gcp-secretmanager 159 | ``` 160 | 161 | 1. Create `MySQL` 162 | 163 | ``` 164 | kubectl apply -f - < show databases; 237 | +--------------------+ 238 | | Database | 239 | +--------------------+ 240 | | information_schema | 241 | | mysql | 242 | | performance_schema | 243 | | sample_db | 244 | | sys | 245 | +--------------------+ 246 | 5 rows in set (0.01 sec) 247 | ``` 248 | 249 | ```sql 250 | mysql> select User, Host from mysql.user where User = 'sample-user'; 251 | +-------------+------+ 252 | | User | Host | 253 | +-------------+------+ 254 | | sample-user | % | 255 | +-------------+------+ 256 | 1 row in set (0.01 sec) 257 | ``` 258 | 259 | ## 4. Clean up 260 | 261 | ### 4.1. Kubernetes resources 262 | 263 | Custom resources 264 | ``` 265 | kubectl delete mysqldb sample-db 266 | kubectl delete mysqluser sample-user 267 | kubectl delete mysql $INSTANCE_NAME 268 | ``` 269 | 270 | Uninstall `mysql-operator` 271 | 272 | ``` 273 | helm uninstall mysql-operator -n $NAMESPACE 274 | ``` 275 | 276 | ### 4.2. GCP resources 277 | 278 | ``` 279 | gcloud container clusters delete $GKE_CLUSTER_NAME --location $REGION 280 | gcloud sql instances delete ${INSTANCE_NAME} --project ${PROJECT} 281 | gcloud iam service-accounts delete ${SA_NAME}@${PROJECT}.iam.gserviceaccount.com --project ${PROJECT} 282 | gcloud secrets delete $SECRET_NAME --project ${PROJECT} 283 | ``` 284 | -------------------------------------------------------------------------------- /docs/usage/install-with-helm.md: -------------------------------------------------------------------------------- 1 | # Install with Helm 2 | 3 | ## 1. Preparation 4 | 5 | ``` 6 | helm repo add nakamasato https://nakamasato.github.io/helm-charts 7 | helm repo update 8 | ``` 9 | 10 | ## 2. Install 11 | 12 | ### 2.1. Install without GCP SecretManager 13 | 14 | ``` 15 | helm install mysql-operator nakamasato/mysql-operator 16 | ``` 17 | 18 | ### 2.2. Install with GCP credentials json 19 | 20 | 1. Check the yaml to be applied with the `template` command 21 | 22 | ``` 23 | helm template mysql-operator nakamasato/mysql-operator --set adminUserSecretType=gcp --set gcpProjectId=$PROJECT_ID 24 | ``` 25 | 26 | Check point: 27 | - [ ] secret is mounted 28 | - [ ] env var `GOOGLE_APPLICATION_CREDENTIALS` is set 29 | - [ ] env var `PROJECT_ID` is set 30 | 31 | 1. Install 32 | 33 | ``` 34 | helm install mysql-operator nakamasato/mysql-operator --set adminUserSecretType=gcp --set gcpProjectId=$PROJECT_ID --generate-name 35 | ``` 36 | 37 | ### 2.3. Install without GCP credentials json (e.g. Run on GCP resource) 38 | 39 | ``` 40 | helm install mysql-operator ./charts/mysql-operator \ 41 | --dry-run \ 42 | --set adminUserSecretType=gcp \ 43 | --set gcpServiceAccount=${SA_NAME}@${PROJECT}.iam.gserviceaccount.com \ 44 | --set gcpProjectId=$PROJECT \ 45 | --namespace mysql-operator 46 | ``` 47 | 48 | For more details, [GCP SecretManager](gcp-secretmanager.md) 49 | 50 | ## 3. Upgrade 51 | 52 | When you want to modify helm release (start operator with new settings or arguments), you can upgrade an existing release. 53 | 54 | 1. Get target release 55 | ``` 56 | helm list 57 | ``` 58 | 1. Upgrade 59 | ``` 60 | helm upgrade mysql-operator nakamasato/mysql-operator --set adminUserSecretType=gcp --set gcpProjectId=$PROJECT_ID 61 | ``` 62 | 63 | ## 4. Uninstall 64 | 65 | 1. Check helm release to uninstall 66 | ``` 67 | helm list 68 | NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION 69 | mysql-operator default 2 2023-04-08 12:38:58.65552 +0900 JST deployed mysql-operator-0.1.0 v0.2.0 70 | ``` 71 | 1. Uninstall 72 | ``` 73 | helm uninstall mysql-operator 74 | ``` 75 | -------------------------------------------------------------------------------- /docs/usage/schema-migration.md: -------------------------------------------------------------------------------- 1 | # Schema Migration 2 | 3 | Schema migration feature uses https://github.com/golang-migrate/migrate but the supported feature in mysql-operator is limited. 4 | Currently, only [GitHub source](https://github.com/golang-migrate/migrate/tree/master/source/github) is supported. 5 | 6 | ## Usage 7 | 8 | 1. Prepare schema files. 9 | Example: 10 | ```sql 11 | CREATE TABLE test_table (id int, name varchar(10)); 12 | 13 | ``` 14 | 15 | ```sql 16 | DROP TABLE test_table; 17 | ``` 18 | 19 | https://github.com/nakamasato/mysql-operator/tree/96dc1eeaf00c8afb42f1c9b63859ff57c440e584/config/sample-migrations 20 | 21 | 1. Create MySQLDB yaml with `schemaMigrationFromGitHub` 22 | 23 | ```yaml 24 | apiVersion: mysql.nakamasato.com/v1alpha1 25 | kind: MySQLDB 26 | metadata: 27 | labels: 28 | app.kubernetes.io/name: mysqldb 29 | app.kubernetes.io/instance: mysqldb-sample 30 | app.kubernetes.io/part-of: mysql-operator 31 | app.kubernetes.io/managed-by: kustomize 32 | app.kubernetes.io/created-by: mysql-operator 33 | name: sample-db # this is not a name for MySQL database but just a Kubernetes object name 34 | spec: 35 | dbName: sample_db # this is MySQL database name 36 | mysqlName: mysql-sample 37 | schemaMigrationFromGitHub: 38 | owner: nakamasato 39 | repo: mysql-operator 40 | path: config/sample-migrations 41 | ref: 96dc1eeaf00c8afb42f1c9b63859ff57c440e584 # (optional) you can write branch, tag, sha 42 | ``` 43 | 44 | This configuration will generate `"github://nakamasato/mysql-operator/config/sample-migrations#96dc1eeaf00c8afb42f1c9b63859ff57c440e584"` as `sourceUrl` for [source/github](https://github.com/golang-migrate/migrate/tree/master/source/github) 45 | 46 | 1. Run mysql & mysql-operator 47 | 48 | ``` 49 | docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=password --rm mysql:8 50 | ``` 51 | 52 | ```bash 53 | make install run # to be updated with helm command 54 | ``` 55 | 56 | 1. Create resources 57 | 58 | ``` 59 | kubectl apply -k config/samples 60 | ``` 61 | 62 | 1. Check `test_table` is created. 63 | 64 | ``` 65 | docker exec -it $(docker ps | grep mysql | head -1 |awk '{print $1}') mysql -uroot -ppassword 66 | ``` 67 | 68 | ```sql 69 | mysql> use sample_db; 70 | Reading table information for completion of table and column names 71 | You can turn off this feature to get a quicker startup with -A 72 | 73 | Database changed 74 | mysql> show tables; 75 | +---------------------+ 76 | | Tables_in_sample_db | 77 | +---------------------+ 78 | | schema_migrations | 79 | | test_table | 80 | +---------------------+ 81 | 2 rows in set (0.00 sec) 82 | ``` 83 | 84 | 1. Clean up 85 | 86 | ``` 87 | kubectl delete -k config/samples 88 | ``` 89 | -------------------------------------------------------------------------------- /e2e/kind-config.yml: -------------------------------------------------------------------------------- 1 | apiVersion: kind.x-k8s.io/v1alpha4 2 | kind: Cluster 3 | nodes: 4 | - role: control-plane 5 | extraPortMappings: 6 | - containerPort: 30306 7 | hostPort: 30306 # enable to access to mysql container from outside the cluster 8 | listenAddress: "0.0.0.0" 9 | protocol: tcp 10 | -------------------------------------------------------------------------------- /e2e/kind.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | ) 11 | 12 | type Kind struct { 13 | Ctx context.Context 14 | Name string 15 | KubeconfigPath string 16 | LazyMode bool 17 | } 18 | 19 | func (k *Kind) createCluster() (bool, error) { 20 | isRunning := k.checkCluster() 21 | if k.LazyMode && isRunning { 22 | fmt.Println("kind cluster is already running") 23 | return false, nil 24 | } 25 | cmd := exec.CommandContext( 26 | k.Ctx, 27 | "kind", 28 | "create", 29 | "cluster", 30 | "--name", 31 | k.Name, 32 | "--kubeconfig", 33 | k.KubeconfigPath, 34 | "--config", 35 | "kind-config.yml", // to expose node port 36 | "--wait", // block until the control plane reaches a ready status 37 | "60s", 38 | ) 39 | cmd.Stdout = os.Stdout 40 | cmd.Stderr = os.Stderr 41 | fmt.Println("start creating kind cluster") 42 | return true, cmd.Run() 43 | } 44 | 45 | func (k *Kind) checkCluster() bool { 46 | out, err := exec.CommandContext( 47 | k.Ctx, 48 | "kind", 49 | "get", 50 | "clusters", 51 | ).Output() 52 | 53 | if err != nil { 54 | return false 55 | } 56 | clusters := strings.Split(string(out), "\n") 57 | return stringInSlice(k.Name, clusters) 58 | } 59 | 60 | func stringInSlice(a string, list []string) bool { 61 | for _, b := range list { 62 | if b == a { 63 | return true 64 | } 65 | } 66 | return false 67 | } 68 | 69 | func (k *Kind) deleteCluster() (bool, error) { 70 | if k.LazyMode { 71 | fmt.Println("keep kind cluster running for next time.") 72 | return false, nil 73 | } 74 | cmd := exec.CommandContext( 75 | k.Ctx, 76 | "kind", 77 | "delete", 78 | "cluster", 79 | "--name", 80 | k.Name, 81 | ) 82 | cmd.Stdout = os.Stdout 83 | cmd.Stderr = os.Stderr 84 | return true, cmd.Run() 85 | } 86 | 87 | func (k *Kind) checkVersion() error { 88 | cmd := exec.Command("kind", "version") 89 | var out bytes.Buffer 90 | cmd.Stdout = &out 91 | err := cmd.Run() 92 | if err != nil { 93 | return err 94 | } 95 | fmt.Printf("kind version: %s\n", out.String()) 96 | return nil 97 | } 98 | 99 | func newKind(ctx context.Context, name, kubeconfigPath string, lazyMode bool) *Kind { 100 | return &Kind{ 101 | Ctx: ctx, 102 | Name: name, 103 | KubeconfigPath: kubeconfigPath, 104 | LazyMode: lazyMode, 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /e2e/skaffold.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | ) 9 | 10 | type Skaffold struct { 11 | KubeconfigPath string 12 | cmd *exec.Cmd 13 | } 14 | 15 | func (s *Skaffold) run(ctx context.Context) error { 16 | fmt.Println("run") 17 | args := []string{"run", "--kubeconfig", s.KubeconfigPath, "--tail"} 18 | s.cmd = exec.CommandContext( 19 | ctx, 20 | "skaffold", 21 | args..., 22 | ) 23 | s.cmd.Stdout = os.Stdout 24 | s.cmd.Stderr = os.Stderr 25 | return s.cmd.Start() // Run in background 26 | } 27 | 28 | func (s *Skaffold) cleanup() error { 29 | fmt.Println("skaffold cleanup") 30 | fmt.Println("skaffold kill process") 31 | errKill := s.cmd.Process.Kill() 32 | s.cmd = exec.Command( 33 | "skaffold", 34 | "delete", 35 | "--kubeconfig", 36 | s.KubeconfigPath, 37 | ) 38 | fmt.Println("skaffold delete") 39 | errRun := s.cmd.Run() 40 | if errKill != nil { 41 | return errKill 42 | } else if errRun != nil { 43 | return errRun 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /e2e/skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v3 2 | kind: Config 3 | metadata: 4 | name: mysql-operator 5 | build: 6 | artifacts: 7 | - image: mysql-operator # this needs to be same as image specified in config/manager/kustomization.yaml 8 | context: .. 9 | docker: 10 | dockerfile: Dockerfile 11 | local: 12 | push: false 13 | useDockerCLI: true 14 | manifests: 15 | kustomize: 16 | paths: 17 | - ../config/crd 18 | - ../config/default 19 | -------------------------------------------------------------------------------- /e2e/suite_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "path" 9 | "testing" 10 | 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | "go.uber.org/zap/zapcore" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 16 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 17 | "k8s.io/client-go/rest" 18 | ctrl "sigs.k8s.io/controller-runtime" 19 | "sigs.k8s.io/controller-runtime/pkg/client" 20 | "sigs.k8s.io/controller-runtime/pkg/client/config" 21 | logf "sigs.k8s.io/controller-runtime/pkg/log" 22 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 23 | 24 | mysqlv1alpha1 "github.com/nakamasato/mysql-operator/api/v1alpha1" 25 | "github.com/nakamasato/mysql-operator/internal/controller" 26 | appsv1 "k8s.io/api/apps/v1" 27 | ) 28 | 29 | const ( 30 | kindName = "mysql-operator-e2e" 31 | kubeconfigPath = "kubeconfig" 32 | mysqlOperatorNamespace = "mysql-operator-system" 33 | mysqlOperatorDeploymentName = "mysql-operator-controller-manager" 34 | ) 35 | 36 | var skaffold *Skaffold 37 | var kind *Kind 38 | var k8sClient client.Client 39 | var cancel context.CancelFunc 40 | 41 | var ( 42 | log = logf.Log.WithName("mysql-operator-e2e") 43 | scheme = runtime.NewScheme() 44 | ) 45 | 46 | func init() { 47 | utilruntime.Must(mysqlv1alpha1.AddToScheme(scheme)) 48 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 49 | } 50 | 51 | func TestE2e(t *testing.T) { 52 | opts := zap.Options{ 53 | Development: true, 54 | TimeEncoder: zapcore.ISO8601TimeEncoder, 55 | } 56 | opts.BindFlags(flag.CommandLine) 57 | flag.Parse() 58 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 59 | RegisterFailHandler(Fail) // Use Gomega with Ginkgo 60 | RunSpecs(t, "e2e suite") // tells Ginkgo to start the test suite. 61 | } 62 | 63 | var _ = BeforeSuite(func() { 64 | log.Info("Setup kind cluster and mysql-operator") 65 | // 1. TODO: Check if docker is running. 66 | // 2. TODO: Check if kind is avaialble -> install kind if not available. 67 | 68 | ctx := context.Background() 69 | ctx, cancel = context.WithCancel(ctx) 70 | kind = newKind( 71 | ctx, 72 | kindName, 73 | kubeconfigPath, 74 | true, 75 | ) 76 | // 3. Start up kind cluster. 77 | prepareKind(kind) 78 | 79 | // 4. set k8sclient 80 | mydir, err := os.Getwd() 81 | if err != nil { 82 | fmt.Println(err) 83 | } 84 | if err := os.Setenv("KUBECONFIG", path.Join(mydir, kubeconfigPath)); err != nil { 85 | log.Error(err, "Failed to set KUBECONFIG environment variable") 86 | } 87 | cfg, err := config.GetConfigWithContext("kind-" + kindName) 88 | if err != nil { 89 | log.Error(err, "failed to get rest.Config") 90 | } 91 | setUpK8sClient(cfg) 92 | 93 | deleteMySQLUserIfExist(ctx) 94 | deleteMySQLIfExist(ctx) 95 | 96 | // 5. TODO: Check if skaffold is available -> intall skaffold if not available. 97 | 98 | // 6. Set up skaffold 99 | skaffold = &Skaffold{KubeconfigPath: kubeconfigPath} 100 | 101 | // 7. Deploy CRDs and controllers with skaffold. 102 | err = skaffold.run(ctx) // To check log during running tests 103 | Expect(err).To(BeNil()) 104 | 105 | // 8. Check if mysql-operator is running. 106 | checkMySQLOperator() // check if mysql-operator is running 107 | 108 | // 9. Start debug tool 109 | controllers.StartDebugTool(ctx, cfg, scheme) 110 | fmt.Println("Setup completed") 111 | }) 112 | 113 | var _ = AfterSuite(func() { 114 | fmt.Println("Clean up mysql-operator and kind cluster") 115 | cancel() 116 | // 1. Remove the deployed resources 117 | if err := skaffold.cleanup(); err != nil { 118 | log.Error(err, "failed to clean up skaffold") 119 | } 120 | 121 | // 2. Stop kind cluster 122 | cleanUpKind(kind) 123 | }) 124 | 125 | func setUpK8sClient(cfg *rest.Config) { 126 | err := mysqlv1alpha1.AddToScheme(scheme) 127 | Expect(err).NotTo(HaveOccurred()) 128 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) 129 | Expect(err).NotTo(HaveOccurred()) 130 | Expect(k8sClient).NotTo(BeNil()) 131 | } 132 | 133 | func prepareKind(kind *Kind) { 134 | // check kind version 135 | err := kind.checkVersion() 136 | if err != nil { 137 | log.Error(err, "failed to check version") 138 | } 139 | 140 | isDeleted, err := kind.deleteCluster() 141 | if err != nil { 142 | log.Error(err, "failed to delete cluster") 143 | } else if isDeleted { 144 | fmt.Println("kind deleted cluster") 145 | } 146 | 147 | // create cluster 148 | isCreated, err := kind.createCluster() 149 | if err != nil { 150 | log.Error(err, "failed to create kind cluster.") 151 | } else if isCreated { 152 | fmt.Printf("kind created '%s'\n", kindName) 153 | } 154 | } 155 | 156 | func cleanUpKind(kind *Kind) { 157 | isDeleted, err := kind.deleteCluster() 158 | if err != nil { 159 | log.Error(err, "failed to clean up cluster") 160 | } else if isDeleted { 161 | fmt.Printf("kind deleted '%s'\n", kindName) 162 | } 163 | } 164 | 165 | func checkMySQLOperator() { 166 | deployment := &appsv1.Deployment{} 167 | Eventually(func() error { 168 | err := k8sClient.Get(context.TODO(), client.ObjectKey{Namespace: mysqlOperatorNamespace, Name: mysqlOperatorDeploymentName}, deployment) 169 | log.Info("waiting until mysqlOperator Deployment is deployed") 170 | return err 171 | }, 3*timeout, interval).Should(BeNil()) 172 | 173 | Eventually(func() bool { 174 | err := k8sClient.Get(context.TODO(), client.ObjectKey{Namespace: mysqlOperatorNamespace, Name: mysqlOperatorDeploymentName}, deployment) 175 | if err != nil { 176 | return false 177 | } 178 | log.Info("waiting until mysqlOperator Pods get ready", "Replicas", *deployment.Spec.Replicas, "AvailableReplicas", deployment.Status.AvailableReplicas) 179 | return deployment.Status.AvailableReplicas == *deployment.Spec.Replicas 180 | }, 3*timeout, interval).Should(BeTrue()) 181 | } 182 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nakamasato/mysql-operator 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | cloud.google.com/go/secretmanager v1.14.7 9 | github.com/go-sql-driver/mysql v1.9.2 10 | github.com/golang-migrate/migrate/v4 v4.18.3 11 | github.com/nakamasato/test-db-driver v0.0.0-20230330121357-46698833afb6 12 | github.com/onsi/ginkgo/v2 v2.23.4 13 | github.com/onsi/gomega v1.37.0 14 | github.com/prometheus/client_golang v1.22.0 15 | go.uber.org/zap v1.27.0 16 | k8s.io/api v0.33.1 17 | k8s.io/apimachinery v0.33.1 18 | k8s.io/client-go v0.33.1 19 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 20 | sigs.k8s.io/controller-runtime v0.21.0 21 | ) 22 | 23 | require ( 24 | cloud.google.com/go/auth v0.16.0 // indirect 25 | cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect 26 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 27 | cloud.google.com/go/iam v1.5.0 // indirect 28 | filippo.io/edwards25519 v1.1.0 // indirect 29 | github.com/beorn7/perks v1.0.1 // indirect 30 | github.com/blang/semver/v4 v4.0.0 // indirect 31 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 32 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 33 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 34 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 35 | github.com/felixge/httpsnoop v1.0.4 // indirect 36 | github.com/fsnotify/fsnotify v1.7.0 // indirect 37 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 38 | github.com/go-logr/logr v1.4.2 // indirect 39 | github.com/go-logr/stdr v1.2.2 // indirect 40 | github.com/go-logr/zapr v1.3.0 // indirect 41 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 42 | github.com/go-openapi/jsonreference v0.20.2 // indirect 43 | github.com/go-openapi/swag v0.23.0 // indirect 44 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 45 | github.com/gogo/protobuf v1.3.2 // indirect 46 | github.com/golang/protobuf v1.5.4 // indirect 47 | github.com/google/btree v1.1.3 // indirect 48 | github.com/google/gnostic-models v0.6.9 // indirect 49 | github.com/google/go-cmp v0.7.0 // indirect 50 | github.com/google/go-github/v39 v39.2.0 // indirect 51 | github.com/google/go-querystring v1.1.0 // indirect 52 | github.com/google/gofuzz v1.2.0 // indirect 53 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 54 | github.com/google/s2a-go v0.1.9 // indirect 55 | github.com/google/uuid v1.6.0 // indirect 56 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect 57 | github.com/googleapis/gax-go/v2 v2.14.1 // indirect 58 | github.com/hashicorp/errwrap v1.1.0 // indirect 59 | github.com/hashicorp/go-multierror v1.1.1 // indirect 60 | github.com/josharian/intern v1.0.0 // indirect 61 | github.com/json-iterator/go v1.1.12 // indirect 62 | github.com/klauspost/compress v1.18.0 // indirect 63 | github.com/kylelemons/godebug v1.1.0 // indirect 64 | github.com/mailru/easyjson v0.7.7 // indirect 65 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 66 | github.com/modern-go/reflect2 v1.0.2 // indirect 67 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 68 | github.com/pkg/errors v0.9.1 // indirect 69 | github.com/prometheus/client_model v0.6.1 // indirect 70 | github.com/prometheus/common v0.62.0 // indirect 71 | github.com/prometheus/procfs v0.15.1 // indirect 72 | github.com/spf13/pflag v1.0.5 // indirect 73 | github.com/x448/float16 v0.8.4 // indirect 74 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 75 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect 76 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 77 | go.opentelemetry.io/otel v1.35.0 // indirect 78 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 79 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 80 | go.uber.org/atomic v1.10.0 // indirect 81 | go.uber.org/automaxprocs v1.6.0 // indirect 82 | go.uber.org/multierr v1.11.0 // indirect 83 | golang.org/x/crypto v0.37.0 // indirect 84 | golang.org/x/net v0.39.0 // indirect 85 | golang.org/x/oauth2 v0.29.0 // indirect 86 | golang.org/x/sync v0.13.0 // indirect 87 | golang.org/x/sys v0.32.0 // indirect 88 | golang.org/x/term v0.31.0 // indirect 89 | golang.org/x/text v0.24.0 // indirect 90 | golang.org/x/time v0.11.0 // indirect 91 | golang.org/x/tools v0.31.0 // indirect 92 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 93 | google.golang.org/api v0.229.0 // indirect 94 | google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect 95 | google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect 96 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect 97 | google.golang.org/grpc v1.71.1 // indirect 98 | google.golang.org/protobuf v1.36.6 // indirect 99 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 100 | gopkg.in/inf.v0 v0.9.1 // indirect 101 | gopkg.in/yaml.v3 v3.0.1 // indirect 102 | k8s.io/apiextensions-apiserver v0.33.0 // indirect 103 | k8s.io/klog/v2 v2.130.1 // indirect 104 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 105 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 106 | sigs.k8s.io/randfill v1.0.0 // indirect 107 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 108 | sigs.k8s.io/yaml v1.4.0 // indirect 109 | ) 110 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 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 | -------------------------------------------------------------------------------- /internal/controller/mysql_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/types" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 15 | 16 | mysqlv1alpha1 "github.com/nakamasato/mysql-operator/api/v1alpha1" 17 | internalmysql "github.com/nakamasato/mysql-operator/internal/mysql" 18 | "github.com/nakamasato/mysql-operator/internal/secret" 19 | ) 20 | 21 | var _ = Describe("MySQL controller", func() { 22 | 23 | ctx := context.Background() 24 | var stopFunc func() 25 | var mySQLClients internalmysql.MySQLClients 26 | 27 | BeforeEach(func() { 28 | k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ 29 | Scheme: scheme, 30 | }) 31 | Expect(err).ToNot(HaveOccurred()) 32 | 33 | // set index for mysqluser with spec.mysqlName 34 | cache := k8sManager.GetCache() 35 | indexFunc := func(obj client.Object) []string { 36 | return []string{obj.(*mysqlv1alpha1.MySQLUser).Spec.MysqlName} 37 | } 38 | if err := cache.IndexField(ctx, &mysqlv1alpha1.MySQLUser{}, "spec.mysqlName", indexFunc); err != nil { 39 | panic(err) 40 | } 41 | indexFunc = func(obj client.Object) []string { 42 | return []string{obj.(*mysqlv1alpha1.MySQLDB).Spec.MysqlName} 43 | } 44 | if err := cache.IndexField(ctx, &mysqlv1alpha1.MySQLDB{}, "spec.mysqlName", indexFunc); err != nil { 45 | panic(err) 46 | } 47 | 48 | mySQLClients = internalmysql.MySQLClients{} 49 | reconciler := &MySQLReconciler{ 50 | Client: k8sManager.GetClient(), 51 | Scheme: k8sManager.GetScheme(), 52 | MySQLClients: mySQLClients, 53 | MySQLDriverName: "testdbdriver", 54 | SecretManagers: map[string]secret.SecretManager{"raw": secret.RawSecretManager{}}, 55 | } 56 | err = ctrl.NewControllerManagedBy(k8sManager). 57 | For(&mysqlv1alpha1.MySQL{}). 58 | Owns(&mysqlv1alpha1.MySQLUser{}). 59 | Owns(&mysqlv1alpha1.MySQLDB{}). 60 | Named(fmt.Sprintf("mysql-test-%d", time.Now().UnixNano())). 61 | Complete(reconciler) 62 | Expect(err).ToNot(HaveOccurred()) 63 | 64 | ctx, cancel := context.WithCancel(ctx) 65 | stopFunc = cancel 66 | go func() { 67 | err = k8sManager.Start(ctx) 68 | Expect(err).ToNot(HaveOccurred()) 69 | }() 70 | time.Sleep(100 * time.Millisecond) 71 | }) 72 | 73 | AfterEach(func() { 74 | stopFunc() 75 | time.Sleep(100 * time.Millisecond) 76 | }) 77 | 78 | Context("With available MySQL", func() { 79 | BeforeEach(func() { 80 | cleanUpMySQLUser(ctx, k8sClient, Namespace) 81 | cleanUpMySQLDB(ctx, k8sClient, Namespace) 82 | cleanUpMySQL(ctx, k8sClient, Namespace) 83 | 84 | // Create MySQL 85 | mysql = &mysqlv1alpha1.MySQL{ 86 | TypeMeta: metav1.TypeMeta{APIVersion: APIVersion, Kind: "MySQL"}, 87 | ObjectMeta: metav1.ObjectMeta{Name: MySQLName, Namespace: Namespace}, 88 | Spec: mysqlv1alpha1.MySQLSpec{Host: "nonexistinghost", AdminUser: mysqlv1alpha1.Secret{Name: "root", Type: "raw"}, AdminPassword: mysqlv1alpha1.Secret{Name: "password", Type: "raw"}}, 89 | } 90 | Expect(k8sClient.Create(ctx, mysql)).Should(Succeed()) 91 | }) 92 | AfterEach(func() { 93 | cleanUpMySQLUser(ctx, k8sClient, Namespace) 94 | cleanUpMySQLDB(ctx, k8sClient, Namespace) 95 | cleanUpMySQL(ctx, k8sClient, Namespace) 96 | }) 97 | It("Should have status.UserCount=0", func() { 98 | checkMySQLUserCount(ctx, int32(0)) 99 | }) 100 | 101 | It("Should increase status.UserCount by one", func() { 102 | By("By creating a new MySQLUser") 103 | mysqlUser = newMySQLUser(APIVersion, Namespace, MySQLUserName, MySQLName) 104 | Expect(controllerutil.SetOwnerReference(mysql, mysqlUser, scheme)).Should(Succeed()) 105 | Expect(k8sClient.Create(ctx, mysqlUser)).Should(Succeed()) 106 | 107 | checkMySQLUserCount(ctx, int32(1)) 108 | }) 109 | 110 | It("Should increase status.UserCount to two", func() { 111 | By("By creating a new MySQLUser") 112 | mysqlUser = newMySQLUser(APIVersion, Namespace, MySQLUserName, MySQLName) 113 | addOwnerReferenceToMySQL(mysqlUser, mysql) 114 | Expect(k8sClient.Create(ctx, mysqlUser)).Should(Succeed()) 115 | checkMySQLUserCount(ctx, int32(1)) 116 | 117 | By("By creating another MySQLUser") 118 | mysqlUser2 := newMySQLUser(APIVersion, Namespace, "mysql-test-user-2", MySQLName) 119 | addOwnerReferenceToMySQL(mysqlUser2, mysql) 120 | Expect(k8sClient.Create(ctx, mysqlUser2)).Should(Succeed()) 121 | 122 | checkMySQLUserCount(ctx, int32(2)) 123 | }) 124 | 125 | It("Should decrease status.UserCount to zero", func() { 126 | By("By creating a new MySQLUser") 127 | mysqlUser = newMySQLUser(APIVersion, Namespace, MySQLUserName, MySQLName) 128 | // TODO: Check if it's possible to use Expect(controllerutil.SetOwnerReference(mysql, mysqlUser, scheme)).Should(Succeed()) 129 | addOwnerReferenceToMySQL(mysqlUser, mysql) 130 | Expect(k8sClient.Create(ctx, mysqlUser)).Should(Succeed()) 131 | checkMySQLUserCount(ctx, int32(1)) 132 | 133 | By("By deleting the MySQLUser") 134 | Expect(k8sClient.Delete(ctx, mysqlUser)).Should(Succeed()) 135 | 136 | checkMySQLUserCount(ctx, int32(0)) 137 | }) 138 | 139 | It("Should increase status.DBCount by one", func() { 140 | By("By creating a new MySQLDB") 141 | mysqlDB = newMySQLDB(APIVersion, Namespace, MySQLDBName, DatabaseName, MySQLName) 142 | Expect(controllerutil.SetOwnerReference(mysql, mysqlDB, scheme)).Should(Succeed()) 143 | Expect(k8sClient.Create(ctx, mysqlDB)).Should(Succeed()) 144 | 145 | checkMySQLDBCount(ctx, int32(1)) 146 | }) 147 | 148 | It("Should have finalizer", func() { 149 | Eventually(func() bool { 150 | err := k8sClient.Get(ctx, client.ObjectKey{Namespace: Namespace, Name: MySQLName}, mysql) 151 | if err != nil { 152 | return false 153 | } 154 | return controllerutil.ContainsFinalizer(mysql, mysqlFinalizer) 155 | }).Should(BeTrue()) 156 | }) 157 | 158 | It("Should have MySQL client for database", func() { 159 | By("By creating a new MySQLDB") 160 | mysqlDB = newMySQLDB(APIVersion, Namespace, MySQLDBName, DatabaseName, MySQLName) 161 | Expect(controllerutil.SetOwnerReference(mysql, mysqlDB, scheme)).Should(Succeed()) 162 | Expect(k8sClient.Create(ctx, mysqlDB)).Should(Succeed()) 163 | 164 | By("Wait until MySQLDB gets ready") 165 | mysqlDB.Status.Phase = "Ready" 166 | Expect(k8sClient.Status().Update(ctx, mysqlDB)).Should(Succeed()) 167 | 168 | Eventually(func() int { return len(mySQLClients) }).Should(Equal(2)) 169 | }) 170 | 171 | It("Should update MySQLClient", func() { 172 | Eventually(func() error { 173 | _, err := mySQLClients.GetClient(mysql.GetKey()) 174 | return err 175 | }).Should(BeNil()) 176 | Eventually(func() int { return len(mySQLClients) }).Should(Equal(1)) 177 | }) 178 | 179 | It("Should clean up MySQLClient", func() { 180 | By("Wait until MySQLClients is updated") 181 | Eventually(func() error { 182 | _, err := mySQLClients.GetClient(mysql.GetKey()) 183 | return err 184 | }).Should(BeNil()) 185 | Eventually(func() int { return len(mySQLClients) }).Should(Equal(1)) 186 | 187 | By("By deleting MySQL") 188 | Expect(k8sClient.Delete(ctx, mysql)).Should(Succeed()) 189 | 190 | Eventually(func() int { return len(mySQLClients) }).Should(Equal(0)) 191 | }) 192 | }) 193 | }) 194 | 195 | func checkMySQLUserCount(ctx context.Context, expectedUserCount int32) { 196 | Eventually(func() int32 { 197 | err := k8sClient.Get(ctx, types.NamespacedName{Name: MySQLName, Namespace: Namespace}, mysql) 198 | if err != nil { 199 | return -1 200 | } 201 | return mysql.Status.UserCount 202 | }).Should(Equal(expectedUserCount)) 203 | } 204 | 205 | func checkMySQLDBCount(ctx context.Context, expectedDBCount int32) { 206 | Eventually(func() int32 { 207 | err := k8sClient.Get(ctx, types.NamespacedName{Name: MySQLName, Namespace: Namespace}, mysql) 208 | if err != nil { 209 | return -1 210 | } 211 | return mysql.Status.DBCount 212 | }).Should(Equal(expectedDBCount)) 213 | } 214 | -------------------------------------------------------------------------------- /internal/controller/mysqldb_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "database/sql" 22 | "fmt" 23 | "time" 24 | 25 | "github.com/golang-migrate/migrate/v4" 26 | migratemysql "github.com/golang-migrate/migrate/v4/database/mysql" 27 | _ "github.com/golang-migrate/migrate/v4/source/github" 28 | mysqlv1alpha1 "github.com/nakamasato/mysql-operator/api/v1alpha1" 29 | mysqlinternal "github.com/nakamasato/mysql-operator/internal/mysql" 30 | "k8s.io/apimachinery/pkg/api/errors" 31 | "k8s.io/apimachinery/pkg/runtime" 32 | ctrl "sigs.k8s.io/controller-runtime" 33 | "sigs.k8s.io/controller-runtime/pkg/client" 34 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 35 | "sigs.k8s.io/controller-runtime/pkg/log" 36 | ) 37 | 38 | const ( 39 | mysqlDBFinalizer = "mysqldb.nakamasato.com/finalizer" 40 | mysqlDBPhaseNotReady = "NotReady" 41 | mysqlDBReasonMySQLFetchFailed = "Failed to fetch MySQL" 42 | mysqlDBReasonMySQLConnectionFailed = "Failed to connect to mysql" 43 | mysqlDBPhaseReady = "Ready" 44 | mysqlDBReasonCompleted = "Database successfully created" 45 | ) 46 | 47 | // MySQLDBReconciler reconciles a MySQLDB object 48 | type MySQLDBReconciler struct { 49 | client.Client 50 | Scheme *runtime.Scheme 51 | MySQLClients mysqlinternal.MySQLClients 52 | } 53 | 54 | //+kubebuilder:rbac:groups=mysql.nakamasato.com,resources=mysqldbs,verbs=get;list;watch;create;update;patch;delete 55 | //+kubebuilder:rbac:groups=mysql.nakamasato.com,resources=mysqldbs/status,verbs=get;update;patch 56 | //+kubebuilder:rbac:groups=mysql.nakamasato.com,resources=mysqldbs/finalizers,verbs=update 57 | 58 | // Reconcile function is responsible for managing MySQL database. 59 | // Create database if not exists in the target MySQL and drop it if 60 | // the corresponding object is deleted. 61 | func (r *MySQLDBReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 62 | log := log.FromContext(ctx).WithName("MySQLDBReconciler") 63 | 64 | // 1. Fetch MySQLDB 65 | mysqlDB := &mysqlv1alpha1.MySQLDB{} 66 | err := r.Get(ctx, req.NamespacedName, mysqlDB) 67 | if err != nil { 68 | if errors.IsNotFound(err) { 69 | log.Info("MySQLDB not found", "req.NamespacedName", req.NamespacedName) 70 | return ctrl.Result{}, nil 71 | } 72 | 73 | log.Error(err, "Failed to get MySQLDB") 74 | return ctrl.Result{}, err 75 | } 76 | 77 | // 2. Fetch MySQL 78 | mysql := &mysqlv1alpha1.MySQL{} 79 | if err := r.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: mysqlDB.Spec.MysqlName}, mysql); err != nil { 80 | log.Error(err, "[FetchMySQL] Failed") 81 | mysqlDB.Status.Phase = mysqlDBPhaseNotReady 82 | mysqlDB.Status.Reason = mysqlDBReasonMySQLFetchFailed 83 | if serr := r.Status().Update(ctx, mysqlDB); serr != nil { 84 | log.Error(serr, "Failed to update MySQLDB status", "Name", mysqlDB.Name) 85 | } 86 | return ctrl.Result{}, err 87 | } 88 | 89 | // 3. Get mysqlClient without specifying database 90 | mysqlClient, err := r.MySQLClients.GetClient(mysql.GetKey()) 91 | if err != nil { 92 | log.Error(err, "Failed to get MySQL client", "key", mysqlDB.GetKey()) 93 | return ctrl.Result{RequeueAfter: time.Second}, nil 94 | } 95 | 96 | // 4. finalize if marked as deleted 97 | if !mysqlDB.GetDeletionTimestamp().IsZero() { 98 | if controllerutil.ContainsFinalizer(mysqlDB, mysqlDBFinalizer) { 99 | if err := r.finalizeMySQLDB(ctx, mysqlClient, mysqlDB); err != nil { 100 | return ctrl.Result{}, err 101 | } 102 | if controllerutil.RemoveFinalizer(mysqlDB, mysqlDBFinalizer) { 103 | if err := r.Update(ctx, mysqlDB); err != nil { 104 | return ctrl.Result{}, err 105 | } 106 | } 107 | return ctrl.Result{}, nil 108 | } 109 | return ctrl.Result{}, nil 110 | } 111 | 112 | // 5. Add finalizer 113 | if controllerutil.AddFinalizer(mysqlDB, mysqlDBFinalizer) { 114 | err = r.Update(ctx, mysqlDB) 115 | if err != nil { 116 | return ctrl.Result{}, err 117 | } 118 | } 119 | 120 | // 6. Create database if not exists 121 | res, err := mysqlClient.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", mysqlDB.Spec.DBName)) 122 | if err != nil { 123 | log.Error(err, "[MySQL] Failed to create MySQL database.", "mysql", mysql.Name, "database", mysqlDB.Spec.DBName) 124 | mysqlDB.Status.Phase = mysqlDBPhaseNotReady 125 | mysqlDB.Status.Reason = err.Error() 126 | if serr := r.Status().Update(ctx, mysqlDB); serr != nil { 127 | log.Error(serr, "Failed to update mysqlDB status", "mysqlDB", mysqlDB.Name) 128 | return ctrl.Result{RequeueAfter: time.Second}, nil 129 | } 130 | return ctrl.Result{}, err 131 | } 132 | rows, err := res.RowsAffected() 133 | if err != nil { 134 | log.Error(err, "Failed to get res.RowsAffected") 135 | return ctrl.Result{}, err 136 | } 137 | if rows > 0 { 138 | mysqlDB.Status.Phase = mysqlDBPhaseReady 139 | mysqlDB.Status.Reason = mysqlDBReasonCompleted 140 | if serr := r.Status().Update(ctx, mysqlDB); serr != nil { 141 | log.Error(serr, "Failed to update MySQLDB status", "Name", mysqlDB.Spec.DBName) 142 | return ctrl.Result{RequeueAfter: time.Second}, nil 143 | } 144 | } else { 145 | log.Info("database already exists", "database", mysqlDB.Spec.DBName) 146 | } 147 | 148 | // 7. Get MySQL client for database 149 | mysqlClient, err = r.MySQLClients.GetClient(mysqlDB.GetKey()) 150 | if err != nil { 151 | log.Error(err, "Failed to get MySQL Client", "key", mysqlDB.GetKey()) 152 | return ctrl.Result{}, err 153 | } 154 | 155 | // 6. Migrate database 156 | if mysqlDB.Spec.SchemaMigrationFromGitHub == nil { 157 | return ctrl.Result{}, nil 158 | } 159 | driver, err := migratemysql.WithInstance( // initialize db driver instance 160 | mysqlClient, 161 | &migratemysql.Config{DatabaseName: mysqlDB.Spec.DBName}, 162 | ) 163 | if err != nil { 164 | log.Error(err, "failed to create migratemysql.WithInstance") 165 | return ctrl.Result{}, err 166 | } 167 | 168 | m, err := migrate.NewWithDatabaseInstance( // initialize Migrate with db driver instance 169 | // "github://nakamasato/mysql-operator/config/sample-migrations#enable-to-migrate-schema-with-migrate", // Currently only support GitHub source 170 | mysqlDB.Spec.SchemaMigrationFromGitHub.GetSourceUrl(), 171 | mysqlDB.Spec.DBName, 172 | driver, 173 | ) 174 | if err != nil { 175 | log.Error(err, "failed to initialize NewWithDatabaseInstance") 176 | return ctrl.Result{}, err 177 | } 178 | err = m.Up() // TODO: enable to specify what to do. 179 | if err != nil { 180 | if err.Error() == "no change" { 181 | log.Info("migrate no change") 182 | } else { 183 | log.Error(err, "failed to Up") 184 | return ctrl.Result{}, err 185 | } 186 | } 187 | 188 | version, dirty, err := m.Version() 189 | if err != nil { 190 | return ctrl.Result{}, err 191 | } 192 | log.Info("migrate completed", "version", version, "dirty", dirty) 193 | 194 | mysqlDB.Status.SchemaMigration.Version = version 195 | mysqlDB.Status.SchemaMigration.Dirty = dirty 196 | err = r.Status().Update(ctx, mysqlDB) 197 | if err != nil { 198 | return ctrl.Result{}, err 199 | } 200 | 201 | return ctrl.Result{}, nil 202 | } 203 | 204 | // finalizeMySQLDB drops MySQL database 205 | func (r *MySQLDBReconciler) finalizeMySQLDB(ctx context.Context, mysqlClient *sql.DB, mysqlDB *mysqlv1alpha1.MySQLDB) error { 206 | _, err := mysqlClient.ExecContext(ctx, fmt.Sprintf("DROP DATABASE IF EXISTS %s", mysqlDB.Spec.DBName)) 207 | return err 208 | } 209 | 210 | // SetupWithManager sets up the controller with the Manager. 211 | func (r *MySQLDBReconciler) SetupWithManager(mgr ctrl.Manager) error { 212 | return ctrl.NewControllerManagedBy(mgr). 213 | For(&mysqlv1alpha1.MySQLDB{}). 214 | Complete(r) 215 | } 216 | -------------------------------------------------------------------------------- /internal/controller/mysqldb_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "time" 8 | 9 | mysqlv1alpha1 "github.com/nakamasato/mysql-operator/api/v1alpha1" 10 | . "github.com/nakamasato/mysql-operator/internal/mysql" 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | ctrl "sigs.k8s.io/controller-runtime" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 17 | ) 18 | 19 | var _ = Describe("MySQLDB controller", func() { 20 | It("Should create database", func() { 21 | db, err := sql.Open("testdbdriver", "test") 22 | Expect(err).ToNot(HaveOccurred()) 23 | defer func() { 24 | if err := db.Close(); err != nil { 25 | GinkgoT().Logf("Failed to close database connection: %v", err) 26 | } 27 | }() 28 | ctx := context.Background() 29 | _, err = db.ExecContext(ctx, "CREATE DATABASE IF NOT EXISTS test_database") 30 | Expect(err).ToNot(HaveOccurred()) 31 | }) 32 | Context("With available MySQL", func() { 33 | ctx := context.Background() 34 | var stopFunc func() 35 | var close func() error 36 | BeforeEach(func() { 37 | k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{Scheme: scheme}) 38 | Expect(err).ToNot(HaveOccurred()) 39 | db, err := sql.Open("testdbdriver", "test") 40 | close = db.Close 41 | Expect(err).ToNot(HaveOccurred()) 42 | reconciler := &MySQLDBReconciler{ 43 | Client: k8sManager.GetClient(), 44 | Scheme: k8sManager.GetScheme(), 45 | MySQLClients: MySQLClients{fmt.Sprintf("%s-%s", Namespace, MySQLName): db}, 46 | } 47 | err = ctrl.NewControllerManagedBy(k8sManager). 48 | For(&mysqlv1alpha1.MySQLDB{}). 49 | Named(fmt.Sprintf("mysqldb-test-%d", time.Now().UnixNano())). 50 | Complete(reconciler) 51 | Expect(err).ToNot(HaveOccurred()) 52 | ctx, cancel := context.WithCancel(ctx) 53 | stopFunc = cancel 54 | go func() { 55 | err = k8sManager.Start(ctx) 56 | Expect(err).ToNot(HaveOccurred()) 57 | }() 58 | cleanUpMySQL(ctx, k8sClient, Namespace) 59 | }) 60 | It("Should have mysqlDBFinalizer", func() { 61 | By("By creating a new MySQL") 62 | mysql = &mysqlv1alpha1.MySQL{ 63 | TypeMeta: metav1.TypeMeta{APIVersion: APIVersion, Kind: "MySQL"}, 64 | ObjectMeta: metav1.ObjectMeta{Name: MySQLName, Namespace: Namespace}, 65 | Spec: mysqlv1alpha1.MySQLSpec{ 66 | Host: "nonexistinghost", 67 | AdminUser: mysqlv1alpha1.Secret{Name: "root", Type: "raw"}, 68 | AdminPassword: mysqlv1alpha1.Secret{Name: "password", Type: "raw"}, 69 | }, 70 | } 71 | Expect(k8sClient.Create(ctx, mysql)).Should(Succeed()) 72 | mysqlDB := &mysqlv1alpha1.MySQLDB{ 73 | TypeMeta: metav1.TypeMeta{APIVersion: APIVersion, Kind: "MySQLDB"}, 74 | ObjectMeta: metav1.ObjectMeta{Name: "sample-db", Namespace: Namespace}, 75 | Spec: mysqlv1alpha1.MySQLDBSpec{DBName: "sample_db", MysqlName: MySQLName}, 76 | } 77 | Expect(k8sClient.Create(ctx, mysqlDB)).Should(Succeed()) 78 | Eventually(func() bool { 79 | err := k8sClient.Get(ctx, client.ObjectKey{Namespace: Namespace, Name: "sample-db"}, mysqlDB) 80 | if err != nil { 81 | return false 82 | } 83 | return controllerutil.ContainsFinalizer(mysqlDB, mysqlDBFinalizer) 84 | }).Should(BeTrue()) 85 | }) 86 | 87 | It("Should be ready", func() { 88 | By("By creating a new MySQL") 89 | mysql = &mysqlv1alpha1.MySQL{ 90 | TypeMeta: metav1.TypeMeta{APIVersion: APIVersion, Kind: "MySQL"}, 91 | ObjectMeta: metav1.ObjectMeta{Name: MySQLName, Namespace: Namespace}, 92 | Spec: mysqlv1alpha1.MySQLSpec{ 93 | Host: "nonexistinghost", 94 | AdminUser: mysqlv1alpha1.Secret{Name: "root", Type: "raw"}, 95 | AdminPassword: mysqlv1alpha1.Secret{Name: "password", Type: "raw"}, 96 | }, 97 | } 98 | Expect(k8sClient.Create(ctx, mysql)).Should(Succeed()) 99 | mysqlDB := &mysqlv1alpha1.MySQLDB{ 100 | TypeMeta: metav1.TypeMeta{APIVersion: APIVersion, Kind: "MySQLDB"}, 101 | ObjectMeta: metav1.ObjectMeta{Name: "sample-db", Namespace: Namespace}, 102 | Spec: mysqlv1alpha1.MySQLDBSpec{DBName: "sample_db", MysqlName: MySQLName}, 103 | } 104 | Expect(k8sClient.Create(ctx, mysqlDB)).Should(Succeed()) 105 | Eventually(func() string { 106 | err := k8sClient.Get(ctx, client.ObjectKey{Namespace: Namespace, Name: "sample-db"}, mysqlDB) 107 | if err != nil { 108 | return "" 109 | } 110 | return mysqlDB.Status.Phase 111 | }).Should(Equal(mysqlDBPhaseReady)) 112 | }) 113 | 114 | It("Should be NotReady without MySQL", func() { 115 | mysqlDB := &mysqlv1alpha1.MySQLDB{ 116 | TypeMeta: metav1.TypeMeta{APIVersion: APIVersion, Kind: "MySQLDB"}, 117 | ObjectMeta: metav1.ObjectMeta{Name: "sample-db", Namespace: Namespace}, 118 | Spec: mysqlv1alpha1.MySQLDBSpec{DBName: "sample_db", MysqlName: MySQLName}, 119 | } 120 | Expect(k8sClient.Create(ctx, mysqlDB)).Should(Succeed()) 121 | Eventually(func() string { 122 | err := k8sClient.Get(ctx, client.ObjectKey{Namespace: Namespace, Name: "sample-db"}, mysqlDB) 123 | if err != nil { 124 | return "" 125 | } 126 | return mysqlDB.Status.Phase 127 | }).Should(Equal(mysqlDBPhaseNotReady)) 128 | }) 129 | 130 | AfterEach(func() { 131 | cleanUpMySQLDB(ctx, k8sClient, Namespace) 132 | cleanUpMySQL(ctx, k8sClient, Namespace) 133 | stopFunc() 134 | err := close() 135 | Expect(err).NotTo(HaveOccurred()) 136 | time.Sleep(100 * time.Millisecond) 137 | }) 138 | }) 139 | }) 140 | -------------------------------------------------------------------------------- /internal/controller/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "path/filepath" 22 | "testing" 23 | "time" 24 | 25 | mysqlv1alpha1 "github.com/nakamasato/mysql-operator/api/v1alpha1" 26 | testdbdriver "github.com/nakamasato/test-db-driver" 27 | . "github.com/onsi/ginkgo/v2" 28 | . "github.com/onsi/gomega" 29 | "k8s.io/apimachinery/pkg/runtime" 30 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 31 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 32 | "k8s.io/client-go/rest" 33 | "sigs.k8s.io/controller-runtime/pkg/client" 34 | "sigs.k8s.io/controller-runtime/pkg/envtest" 35 | logf "sigs.k8s.io/controller-runtime/pkg/log" 36 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 37 | //+kubebuilder:scaffold:imports 38 | ) 39 | 40 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 41 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 42 | 43 | var cfg *rest.Config 44 | var k8sClient client.Client 45 | var testEnv *envtest.Environment 46 | var mysql *mysqlv1alpha1.MySQL 47 | var mysqlUser *mysqlv1alpha1.MySQLUser 48 | var mysqlDB *mysqlv1alpha1.MySQLDB 49 | 50 | const ( 51 | APIVersion = "mysql.nakamasato.com/v1alpha1" 52 | MySQLName = "test-mysql" 53 | MySQLUserName = "test-mysql-user" 54 | MySQLDBName = "test-mysql-db" // Kubernetes object name 55 | DatabaseName = "test_db" // MySQL database name 56 | Namespace = "default" 57 | interval = time.Millisecond * 250 58 | ) 59 | 60 | var scheme = runtime.NewScheme() 61 | var cancel context.CancelFunc 62 | 63 | func init() { 64 | utilruntime.Must(mysqlv1alpha1.AddToScheme(scheme)) 65 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 66 | testdbdriver.Register("testdbdriver", "testdb", "test_user@/test_db") 67 | } 68 | 69 | func TestAPIs(t *testing.T) { 70 | RegisterFailHandler(Fail) 71 | 72 | RunSpecs(t, "Controller Suite") 73 | } 74 | 75 | var _ = BeforeSuite(func() { 76 | _, cancel = context.WithCancel(context.Background()) 77 | 78 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 79 | 80 | By("bootstrapping test environment") 81 | testEnv = &envtest.Environment{ 82 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, 83 | ErrorIfCRDPathMissing: true, 84 | } 85 | 86 | var err error 87 | cfg, err = testEnv.Start() 88 | Expect(err).NotTo(HaveOccurred()) 89 | Expect(cfg).NotTo(BeNil()) 90 | 91 | //+kubebuilder:scaffold:scheme 92 | 93 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) 94 | Expect(err).NotTo(HaveOccurred()) 95 | Expect(k8sClient).NotTo(BeNil()) 96 | }) 97 | 98 | var _ = AfterSuite(func() { 99 | By("tearing down the test environment") 100 | cancel() 101 | err := testEnv.Stop() 102 | Expect(err).NotTo(HaveOccurred()) 103 | }) 104 | -------------------------------------------------------------------------------- /internal/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "sigs.k8s.io/controller-runtime/pkg/metrics" 6 | ) 7 | 8 | const MetricsNamespace = "mysqloperator" 9 | 10 | type MysqlUserTotalAdaptor struct { 11 | metric prometheus.Counter 12 | } 13 | 14 | func (m MysqlUserTotalAdaptor) Increment() { 15 | m.metric.Inc() 16 | } 17 | 18 | var ( 19 | mysqlUserCreatedTotal = prometheus.NewCounter( 20 | prometheus.CounterOpts{ 21 | Namespace: MetricsNamespace, 22 | Name: "mysql_user_created_total", 23 | Help: "Number of created MySQL User", 24 | }, 25 | ) 26 | 27 | mysqlUserDeletedTotal = prometheus.NewCounter( 28 | prometheus.CounterOpts{ 29 | Namespace: MetricsNamespace, 30 | Name: "mysql_user_deleted_total", 31 | Help: "Number of deleted MySQL User", 32 | }, 33 | ) 34 | 35 | MysqlUserCreatedTotal *MysqlUserTotalAdaptor = &MysqlUserTotalAdaptor{metric: mysqlUserCreatedTotal} 36 | MysqlUserDeletedTotal *MysqlUserTotalAdaptor = &MysqlUserTotalAdaptor{metric: mysqlUserDeletedTotal} 37 | ) 38 | 39 | func init() { 40 | // Register custom metrics with the global prometheus registry 41 | metrics.Registry.MustRegister( 42 | mysqlUserCreatedTotal, 43 | mysqlUserDeletedTotal, 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /internal/metrics/metrics_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/prometheus/client_golang/prometheus/testutil" 7 | ) 8 | 9 | func TestMySQLUserCreatedMetrics(t *testing.T) { 10 | MysqlUserCreatedTotal.Increment() 11 | actual := testutil.ToFloat64(mysqlUserCreatedTotal) 12 | assertFloat64(t, float64(1), actual) 13 | 14 | MysqlUserCreatedTotal.Increment() 15 | actual = testutil.ToFloat64(mysqlUserCreatedTotal) 16 | assertFloat64(t, float64(2), actual) 17 | } 18 | 19 | func TestMySQLUserDeletedMetrics(t *testing.T) { 20 | MysqlUserDeletedTotal.Increment() 21 | actual := testutil.ToFloat64(mysqlUserDeletedTotal) 22 | assertFloat64(t, float64(1), actual) 23 | 24 | MysqlUserDeletedTotal.Increment() 25 | actual = testutil.ToFloat64(mysqlUserDeletedTotal) 26 | assertFloat64(t, float64(2), actual) 27 | } 28 | 29 | func assertFloat64(t *testing.T, expected, actual float64) { 30 | if actual != expected { 31 | t.Errorf("value is not %f", expected) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/mysql/mysql.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | ) 7 | 8 | type MySQLClients map[string]*sql.DB 9 | 10 | var ErrMySQLClientNotFound = errors.New("MySQL client not found") 11 | 12 | func (m MySQLClients) GetClient(key string) (*sql.DB, error) { 13 | mysqlClient, ok := m[key] 14 | if ok { 15 | return mysqlClient, nil 16 | } else { 17 | return nil, ErrMySQLClientNotFound 18 | } 19 | } 20 | 21 | // Cloase a MySQL client 22 | func (m MySQLClients) Close(name string) error { 23 | mysqlClient, ok := m[name] 24 | if !ok { 25 | return ErrMySQLClientNotFound 26 | } 27 | if err := mysqlClient.Close(); err != nil { 28 | return err 29 | } 30 | delete(m, name) 31 | return nil 32 | } 33 | 34 | // Close all MySQL clients. 35 | // Return error immediately when error occurs for a client. 36 | func (m MySQLClients) CloseAll() error { 37 | for name := range m { 38 | err := m.Close(name) 39 | if err != nil { 40 | return err 41 | } 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/secret/gcp_secret_manager.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | secretmanager "cloud.google.com/go/secretmanager/apiv1" 8 | secretmanagerpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" 9 | ) 10 | 11 | type gcpSecretManager struct { 12 | projectId string 13 | client *secretmanager.Client 14 | } 15 | 16 | // Initialize SecretManager with projectId 17 | func NewGCPSecretManager(ctx context.Context, projectId string) (*gcpSecretManager, error) { 18 | c, err := secretmanager.NewClient(ctx) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | if projectId == "" { 24 | return nil, fmt.Errorf("ProjectID must not be empty") 25 | } 26 | 27 | return &gcpSecretManager{ 28 | projectId: projectId, 29 | client: c, 30 | }, nil 31 | } 32 | 33 | // Get latest version from SecretManager 34 | func (s gcpSecretManager) GetSecret(ctx context.Context, name string) (string, error) { 35 | res, err := s.client.AccessSecretVersion( 36 | ctx, 37 | &secretmanagerpb.AccessSecretVersionRequest{ 38 | Name: fmt.Sprintf("projects/%s/secrets/%s/versions/latest", s.projectId, name), 39 | }, 40 | ) 41 | if err != nil { 42 | return "", err 43 | } 44 | return string(res.Payload.Data), nil 45 | } 46 | 47 | // Close secretmanager's client 48 | func (s gcpSecretManager) Close() { 49 | if err := s.client.Close(); err != nil { 50 | fmt.Printf("Failed to close GCP Secret Manager client: %v\n", err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/secret/kubernetes_secret_manager.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | "context" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | type k8sSecretManager struct { 11 | namespace string 12 | client client.Client 13 | } 14 | 15 | // Initialize SecretManager with projectId 16 | func Newk8sSecretManager(ctx context.Context, ns string, c client.Client) (*k8sSecretManager, error) { 17 | 18 | return &k8sSecretManager{ 19 | namespace: ns, 20 | client: c, 21 | }, nil 22 | } 23 | 24 | // Get latest version from SecretManager 25 | func (s k8sSecretManager) GetSecret(ctx context.Context, name string) (string, error) { 26 | secret := &corev1.Secret{} 27 | err := s.client.Get(ctx, client.ObjectKey{ 28 | Namespace: s.namespace, 29 | Name: name, 30 | }, secret) 31 | if err != nil { 32 | return "", err 33 | } 34 | stringKey := string(secret.Data["key"]) 35 | return stringKey, nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/secret/raw_secret_manager.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import "context" 4 | 5 | type RawSecretManager struct{} 6 | 7 | // Return the name as secret value 8 | func (r RawSecretManager) GetSecret(ctx context.Context, name string) (string, error) { 9 | return name, nil 10 | } 11 | -------------------------------------------------------------------------------- /internal/secret/secret_manager.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import "context" 4 | 5 | type SecretManager interface { 6 | GetSecret(ctx context.Context, name string) (string, error) 7 | } 8 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "math/rand" 4 | 5 | func GenerateRandomString(n int) string { 6 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 7 | 8 | s := make([]rune, n) 9 | for i := range s { 10 | s[i] = letters[rand.Intn(len(letters))] 11 | } 12 | return string(s) 13 | } 14 | -------------------------------------------------------------------------------- /internal/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func TestGenerateRandomString(t *testing.T) { 6 | t.Run("Generated string length equals to the given value", func(t *testing.T) { 7 | expectedLength := 10 8 | random := GenerateRandomString(expectedLength) 9 | if len(random) != expectedLength { 10 | t.Errorf("Length was expected to be %d, but got %d", expectedLength, len(random)) 11 | } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /kuttl-test.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kuttl.dev/v1beta1 2 | kind: TestSuite 3 | parallel: 4 4 | startKIND: true 5 | timeout: 120 6 | # crdDir: config/crd # doesn't support kustomize? 7 | namespace: default 8 | testDirs: 9 | - tests/e2e/ 10 | startControlPlane: false 11 | kindNodeCache: true 12 | kindContainers: 13 | - mysql-operator:latest 14 | commands: 15 | - command: make install deploy IMG=mysql-operator VERSION=latest # Using local image 16 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: MySQL Operator 2 | theme: 3 | name: material 4 | palette: 5 | primary: cyan 6 | features: 7 | - header.autohide 8 | - navigation.instant 9 | - navigation.tabs 10 | - navigation.indexes 11 | icon: 12 | repo: fontawesome/brands/github 13 | repo_url: https://github.com/nakamasato/mysql-operator 14 | extra: 15 | analytics: 16 | provider: google 17 | property: G-9NTB93RVPJ 18 | markdown_extensions: 19 | - pymdownx.superfences 20 | - pymdownx.magiclink 21 | - pymdownx.tasklist: 22 | custom_checkbox: true 23 | nav: 24 | - Home: 'index.md' 25 | - 'Developer Guide': 26 | - 'API Resources': 'developer-guide/api-resources.md' 27 | - 'Reconciliation': 'developer-guide/reconciliation.md' 28 | - 'Testing': 'developer-guide/testing.md' 29 | - 'Debug': 'developer-guide/debug.md' 30 | - 'Versions': 'developer-guide/versions.md' 31 | - Usage: 32 | - 'Install with Helm': usage/install-with-helm.md 33 | - 'GCP SecretManager': usage/gcp-secretmanager.md 34 | - 'Schema Migration': usage/schema-migration.md 35 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | "github>aquaproj/aqua-renovate-config#2.8.1" 6 | ], 7 | "automerge": true, 8 | "automergeStrategy": "squash" 9 | } 10 | -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v3 2 | kind: Config 3 | metadata: 4 | name: mysql-operator 5 | build: 6 | artifacts: 7 | - image: mysql-operator # this needs to be same as image specified in config/manager/kustomization.yaml 8 | context: . 9 | docker: 10 | dockerfile: Dockerfile 11 | local: 12 | useDockerCLI: true 13 | manifests: 14 | kustomize: 15 | paths: 16 | - config/crd 17 | - config/default 18 | - config/mysql # mysql cluster for testing. todo: will be moved to e2e. 19 | # https://skaffold.dev/docs/testers/custom/ <- no doc about image 20 | # test: 21 | # - image: gcr.io/k8s-skaffold/skaffold-example 22 | # custom: 23 | # - command: echo Hello world!! 24 | -------------------------------------------------------------------------------- /tests/e2e/with-valid-mysql/00-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: mysql 5 | status: 6 | readyReplicas: 1 7 | -------------------------------------------------------------------------------- /tests/e2e/with-valid-mysql/00-mysql-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app: mysql 7 | name: mysql 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: mysql 13 | strategy: {} 14 | template: 15 | metadata: 16 | creationTimestamp: null 17 | labels: 18 | app: mysql 19 | spec: 20 | containers: 21 | - image: mysql:8 22 | name: mysql 23 | # https://hub.docker.com/_/mysql 24 | env: 25 | - name: MYSQL_ROOT_PASSWORD 26 | value: password 27 | resources: {} 28 | status: {} 29 | -------------------------------------------------------------------------------- /tests/e2e/with-valid-mysql/00-mysql-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app: mysql 7 | name: mysql 8 | spec: 9 | ports: 10 | - name: "3306" 11 | port: 3306 12 | protocol: TCP 13 | targetPort: 3306 14 | selector: 15 | app: mysql 16 | type: ClusterIP 17 | status: 18 | loadBalancer: {} 19 | -------------------------------------------------------------------------------- /tests/e2e/with-valid-mysql/01-assert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: mysql-mysql-sample-sample-user 5 | --- 6 | apiVersion: mysql.nakamasato.com/v1alpha1 7 | kind: MySQLUser 8 | metadata: 9 | name: sample-user 10 | --- 11 | apiVersion: mysql.nakamasato.com/v1alpha1 12 | kind: MySQL 13 | metadata: 14 | name: mysql-sample 15 | -------------------------------------------------------------------------------- /tests/e2e/with-valid-mysql/01-create-mysql-user.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kuttl.dev/v1beta1 2 | kind: TestStep 3 | commands: 4 | - command: kubectl apply -k ../../../config/samples-on-k8s 5 | namespaced: true 6 | --------------------------------------------------------------------------------