├── .circleci └── config.yml ├── .github ├── pull_request_template.md └── workflows │ ├── tidy.yml │ ├── zz_generated.add-team-labels.yaml │ ├── zz_generated.add-to-project-board.yaml │ ├── zz_generated.check_values_schema.yaml │ ├── zz_generated.create_release.yaml │ ├── zz_generated.create_release_pr.yaml │ ├── zz_generated.gitleaks.yaml │ └── zz_generated.run_ossf_scorecard.yaml ├── .gitignore ├── .nancy-ignore ├── .nancy-ignore.generated ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── DCO ├── Dockerfile ├── LICENSE ├── Makefile ├── Makefile.gen.app.mk ├── Makefile.gen.go.mk ├── README.md ├── SECURITY.md ├── client └── vault │ ├── error.go │ ├── vault.go │ └── vault_test.go ├── examples ├── README.md └── cert-operator-lab-chart │ ├── Chart.yaml │ ├── templates │ └── deployment.yaml │ └── values.yaml ├── flag ├── flag.go └── service │ ├── app │ └── app.go │ ├── crd │ └── crd.go │ ├── resource │ ├── resource.go │ └── vaultcrt │ │ └── vaultrt.go │ ├── service.go │ └── vault │ ├── config │ ├── config.go │ └── pki │ │ ├── ca │ │ └── ca.go │ │ ├── commonname │ │ └── commonname.go │ │ └── pki.go │ └── vault.go ├── go.mod ├── go.sum ├── helm └── cert-operator │ ├── Chart.yaml │ ├── templates │ ├── _helpers.tpl │ ├── _resource.tpl │ ├── configmap.yaml │ ├── deployment.yaml │ ├── np.yaml │ ├── psp.yaml │ ├── pss-exceptions.yaml │ ├── rbac.yaml │ ├── service-account.yaml │ ├── service.yaml │ └── servicemonitor.yaml │ ├── values.schema.json │ └── values.yaml ├── main.go ├── pkg ├── label │ └── label.go └── project │ └── project.go ├── renovate.json5 ├── server ├── endpoint │ ├── endpoint.go │ └── error.go ├── error.go └── server.go └── service ├── collector ├── error.go ├── set.go └── vault.go ├── controller ├── cert.go ├── error.go ├── key │ ├── error.go │ ├── key.go │ └── key_test.go ├── resource_set.go └── resources │ ├── vaultaccess │ ├── create.go │ ├── delete.go │ ├── error.go │ └── resource.go │ ├── vaultcrt │ ├── create.go │ ├── create_test.go │ ├── current.go │ ├── delete.go │ ├── delete_test.go │ ├── desired.go │ ├── desired_test.go │ ├── error.go │ ├── metric.go │ ├── resource.go │ ├── update.go │ └── update_test.go │ ├── vaultpki │ ├── create.go │ ├── create_test.go │ ├── current.go │ ├── delete.go │ ├── delete_test.go │ ├── desired.go │ ├── desired_test.go │ ├── error.go │ ├── resource.go │ ├── spec.go │ └── update.go │ └── vaultrole │ ├── create.go │ ├── create_test.go │ ├── current.go │ ├── delete.go │ ├── desired.go │ ├── desired_test.go │ ├── error.go │ ├── resource.go │ ├── update.go │ └── update_test.go ├── error.go └── service.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | architect: giantswarm/architect@5.1.1 5 | 6 | workflows: 7 | build: 8 | jobs: 9 | - architect/go-build: 10 | name: go-build 11 | binary: cert-operator 12 | filters: 13 | tags: 14 | only: /^v.*/ 15 | 16 | - architect/push-to-registries: 17 | context: architect 18 | name: push-to-registries 19 | requires: 20 | - go-build 21 | filters: 22 | tags: 23 | only: /^v.*/ 24 | 25 | branches: 26 | ignore: 27 | - main 28 | - master 29 | - architect/push-to-app-catalog: 30 | context: architect 31 | name: push-cert-operator-to-control-plane-app-catalog 32 | app_catalog: control-plane-catalog 33 | app_catalog_test: control-plane-test-catalog 34 | chart: cert-operator 35 | requires: 36 | - push-to-registries 37 | filters: 38 | tags: 39 | only: /^v.*/ 40 | 41 | branches: 42 | ignore: 43 | - main 44 | - master 45 | - architect/push-to-app-collection: 46 | context: architect 47 | name: push-cert-operator-to-aws-app-collection 48 | app_name: cert-operator 49 | app_collection_repo: aws-app-collection 50 | requires: 51 | - push-cert-operator-to-control-plane-app-catalog 52 | filters: 53 | branches: 54 | ignore: /.*/ 55 | tags: 56 | only: /^v.*/ 57 | 58 | - architect/push-to-app-collection: 59 | context: architect 60 | name: push-cert-operator-to-azure-app-collection 61 | app_name: cert-operator 62 | app_collection_repo: azure-app-collection 63 | requires: 64 | - push-cert-operator-to-control-plane-app-catalog 65 | filters: 66 | branches: 67 | ignore: /.*/ 68 | tags: 69 | only: /^v.*/ 70 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Checklist 2 | 3 | - [ ] Update changelog in CHANGELOG.md. 4 | -------------------------------------------------------------------------------- /.github/workflows/tidy.yml: -------------------------------------------------------------------------------- 1 | # Credit: https://github.com/crazy-max/diun 2 | name: auto-go-mod-tidy 3 | 4 | on: 5 | push: 6 | branches: 7 | - 'dependabot/**' 8 | 9 | jobs: 10 | fix: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - 14 | name: Checkout 15 | uses: actions/checkout@v3.6.0 16 | - 17 | # https://github.com/actions/checkout/issues/6 18 | name: Fix detached HEAD 19 | run: git checkout ${GITHUB_REF#refs/heads/} 20 | - 21 | name: Tidy 22 | run: | 23 | rm -f go.sum 24 | go mod tidy 25 | - 26 | name: Set up Git 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | run: | 30 | git config user.name "${GITHUB_ACTOR}" 31 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 32 | git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git 33 | - 34 | name: Commit and push changes 35 | run: | 36 | git add . 37 | if output=$(git status --porcelain) && [ ! -z "$output" ]; then 38 | git commit -m 'Fix go modules' 39 | git push 40 | fi 41 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.add-team-labels.yaml: -------------------------------------------------------------------------------- 1 | name: Add appropriate labels to issue 2 | 3 | on: 4 | issues: 5 | types: [assigned] 6 | 7 | jobs: 8 | build_user_list: 9 | name: Get yaml config of GS users 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Get user-mapping 13 | run: | 14 | mkdir -p artifacts 15 | wget --header "Authorization: token ${{ secrets.ISSUE_AUTOMATION }}" \ 16 | -O artifacts/users.yaml \ 17 | https://raw.githubusercontent.com/giantswarm/github/main/tools/issue-automation/user-mapping.yaml 18 | - name: Upload Artifact 19 | uses: actions/upload-artifact@v4 20 | with: 21 | name: users 22 | path: artifacts/users.yaml 23 | retention-days: 1 24 | 25 | add_label: 26 | name: Add team label when assigned 27 | runs-on: ubuntu-latest 28 | needs: build_user_list 29 | steps: 30 | - uses: actions/download-artifact@v4 31 | id: download-users 32 | with: 33 | name: users 34 | - name: Find team label based on user names 35 | run: | 36 | event_assignee=$(cat $GITHUB_EVENT_PATH | jq -r .assignee.login | tr '[:upper:]' '[:lower:]') 37 | echo "Issue assigned to: ${event_assignee}" 38 | 39 | TEAMS=$(cat ${{steps.download-users.outputs.download-path}}/users.yaml | tr '[:upper:]' '[:lower:]' | yq ".${event_assignee}.teams" -o csv | tr ',' ' ') 40 | 41 | echo "LABEL<> $GITHUB_ENV 42 | for team in ${TEAMS}; do 43 | echo "Team: ${team} | Label: team/${team}" 44 | echo "team/${team}" >> $GITHUB_ENV 45 | done 46 | echo "EOF" >> $GITHUB_ENV 47 | - name: Apply label to issue 48 | if: ${{ env.LABEL != '' && env.LABEL != 'null' && env.LABEL != null }} 49 | uses: actions-ecosystem/action-add-labels@v1 50 | with: 51 | github_token: ${{ secrets.ISSUE_AUTOMATION }} 52 | labels: | 53 | ${{ env.LABEL }} 54 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.add-to-project-board.yaml: -------------------------------------------------------------------------------- 1 | name: Add Issue to Project when assigned 2 | 3 | on: 4 | issues: 5 | types: 6 | - assigned 7 | - labeled 8 | 9 | jobs: 10 | build_user_list: 11 | name: Get yaml config of GS users 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Get user-mapping 15 | run: | 16 | mkdir -p artifacts 17 | wget --header "Authorization: token ${{ secrets.ISSUE_AUTOMATION }}" \ 18 | -O artifacts/users.yaml \ 19 | https://raw.githubusercontent.com/giantswarm/github/main/tools/issue-automation/user-mapping.yaml 20 | - name: Upload Artifact 21 | uses: actions/upload-artifact@v4 22 | with: 23 | name: users 24 | path: artifacts/users.yaml 25 | retention-days: 1 26 | - name: Get label-mapping 27 | run: | 28 | mkdir -p artifacts 29 | wget --header "Authorization: token ${{ secrets.ISSUE_AUTOMATION }}" \ 30 | -O artifacts/labels.yaml \ 31 | https://raw.githubusercontent.com/giantswarm/github/main/tools/issue-automation/label-mapping.yaml 32 | - name: Upload Artifact 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: labels 36 | path: artifacts/labels.yaml 37 | retention-days: 1 38 | 39 | add_to_personal_board: 40 | name: Add issue to personal board 41 | runs-on: ubuntu-latest 42 | needs: build_user_list 43 | if: github.event.action == 'assigned' 44 | steps: 45 | - uses: actions/download-artifact@v4 46 | id: download-users 47 | with: 48 | name: users 49 | - name: Find personal board based on user names 50 | run: | 51 | event_assignee=$(cat $GITHUB_EVENT_PATH | jq -r .assignee.login | tr '[:upper:]' '[:lower:]') 52 | echo "Issue assigned to: ${event_assignee}" 53 | 54 | BOARD=($(cat ${{steps.download-users.outputs.download-path}}/users.yaml | tr '[:upper:]' '[:lower:]' | yq ".${event_assignee}.personalboard")) 55 | echo "Personal board URL: ${BOARD}" 56 | 57 | echo "BOARD=${BOARD}" >> $GITHUB_ENV 58 | - name: Add issue to personal board 59 | if: ${{ env.BOARD != 'null' && env.BOARD != '' && env.BOARD != null }} 60 | uses: actions/add-to-project@main 61 | with: 62 | project-url: ${{ env.BOARD }} 63 | github-token: ${{ secrets.ISSUE_AUTOMATION }} 64 | 65 | add_to_team_board: 66 | name: Add issue to team board 67 | runs-on: ubuntu-latest 68 | needs: build_user_list 69 | if: github.event.action == 'labeled' 70 | steps: 71 | - uses: actions/download-artifact@v4 72 | id: download-labels 73 | with: 74 | name: labels 75 | - name: Find team board based on label 76 | run: | 77 | event_label=$(cat $GITHUB_EVENT_PATH | jq -r .label.name | tr '[:upper:]' '[:lower:]') 78 | echo "Issue labelled with: ${event_label}" 79 | 80 | BOARD=($(cat ${{steps.download-labels.outputs.download-path}}/labels.yaml | tr '[:upper:]' '[:lower:]' | yq ".[\"${event_label}\"].projectboard")) 81 | echo "Team board URL: ${BOARD}" 82 | 83 | echo "BOARD=${BOARD}" >> $GITHUB_ENV 84 | - name: Add issue to team board 85 | if: ${{ env.BOARD != 'null' && env.BOARD != '' && env.BOARD != null }} 86 | uses: actions/add-to-project@main 87 | with: 88 | project-url: ${{ env.BOARD }} 89 | github-token: ${{ secrets.ISSUE_AUTOMATION }} 90 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.check_values_schema.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl@6.23.3 4 | # 5 | name: 'Values and schema' 6 | on: 7 | pull_request: 8 | branches: 9 | - master 10 | - main 11 | paths: 12 | - 'helm/**/values.yaml' # default helm chart values 13 | - 'helm/**/values.schema.json' # schema 14 | - 'helm/**/ci/ci-values.yaml' # overrides for CI (can contain required entries) 15 | 16 | push: {} 17 | 18 | jobs: 19 | check: 20 | name: 'validate values.yaml against values.schema.json' 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Install validator 29 | run: | 30 | wget -q -O ${HOME}/yajsv https://github.com/neilpa/yajsv/releases/download/v1.4.1/yajsv.linux.amd64 31 | chmod +x ${HOME}/yajsv 32 | 33 | - name: 'Check if values.yaml is a valid instance of values.schema.json' 34 | run: | 35 | for chart_yaml in helm/*/Chart.yaml; do 36 | helm_dir="${chart_yaml%/Chart.yaml}" 37 | 38 | if [ ! -f ${helm_dir}/values.schema.json ]; then 39 | echo "Skipping validation for '${helm_dir}' folder, because 'values.schema.json' does not exist..." 40 | continue 41 | fi 42 | 43 | values=${helm_dir}/values.yaml 44 | if [ -f ${helm_dir}/ci/ci-values.yaml ]; then 45 | # merge ci-values.yaml into values.yaml (providing required values) 46 | echo -e "\nMerged values:\n==============" 47 | yq '. *= load("'${helm_dir}'/ci/ci-values.yaml")' ${helm_dir}/values.yaml | tee ${helm_dir}/combined-values.yaml 48 | echo -e "\n==============\n" 49 | values=${helm_dir}/combined-values.yaml 50 | fi 51 | 52 | ${HOME}/yajsv -s ${helm_dir}/values.schema.json ${values} 53 | done 54 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.gitleaks.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl@6.23.3 4 | # 5 | name: gitleaks 6 | 7 | on: [pull_request] 8 | 9 | jobs: 10 | gitleaks: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 14 | with: 15 | fetch-depth: '0' 16 | - name: gitleaks-action 17 | uses: giantswarm/gitleaks-action@main 18 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.run_ossf_scorecard.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl@6.23.3 4 | # 5 | 6 | # This workflow uses actions that are not certified by GitHub. They are provided 7 | # by a third-party and are governed by separate terms of service, privacy 8 | # policy, and support documentation. 9 | 10 | name: Scorecard supply-chain security 11 | on: 12 | # For Branch-Protection check. Only the default branch is supported. See 13 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 14 | branch_protection_rule: 15 | # To guarantee Maintained check is occasionally updated. See 16 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 17 | schedule: 18 | - cron: '15 15 15 * *' 19 | push: 20 | branches: [ "main", "master" ] 21 | workflow_dispatch: {} 22 | 23 | # Declare default permissions as read only. 24 | permissions: read-all 25 | 26 | jobs: 27 | analysis: 28 | name: Scorecard analysis 29 | runs-on: ubuntu-latest 30 | permissions: 31 | # Needed to upload the results to code-scanning dashboard. 32 | security-events: write 33 | # Needed to publish results and get a badge (see publish_results below). 34 | id-token: write 35 | # Uncomment the permissions below if installing in a private repository. 36 | # contents: read 37 | # actions: read 38 | 39 | steps: 40 | - name: "Checkout code" 41 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 42 | with: 43 | persist-credentials: false 44 | 45 | - name: "Run analysis" 46 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 47 | with: 48 | results_file: results.sarif 49 | results_format: sarif 50 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 51 | # - you want to enable the Branch-Protection check on a *public* repository, or 52 | # - you are installing Scorecard on a *private* repository 53 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 54 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 55 | 56 | # Public repositories: 57 | # - Publish results to OpenSSF REST API for easy access by consumers 58 | # - Allows the repository to include the Scorecard badge. 59 | # - See https://github.com/ossf/scorecard-action#publishing-results. 60 | # For private repositories: 61 | # - `publish_results` will always be set to `false`, regardless 62 | # of the value entered here. 63 | publish_results: true 64 | 65 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 66 | # format to the repository Actions tab. 67 | - name: "Upload artifact" 68 | uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 69 | with: 70 | name: SARIF file 71 | path: results.sarif 72 | retention-days: 5 73 | 74 | # Upload the results to GitHub's code scanning dashboard. 75 | - name: "Upload to code-scanning" 76 | uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 77 | with: 78 | sarif_file: results.sarif 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cert-operator* 2 | !helm/cert-operator 3 | TODO 4 | !vendor/** 5 | 6 | /examples/local/*.yaml 7 | !/examples/local/*.tmpl.yaml 8 | .e2e-harness/ 9 | integration/cert-operator-e2e 10 | -------------------------------------------------------------------------------- /.nancy-ignore: -------------------------------------------------------------------------------- 1 | CVE-2024-24786 2 | CVE-2023-48795 3 | -------------------------------------------------------------------------------- /.nancy-ignore.generated: -------------------------------------------------------------------------------- 1 | # This file is generated by https://github.com/giantswarm/github 2 | # Repository specific ignores should be added to .nancy-ignore 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | minimum_pre_commit_version: '2.17' 2 | repos: 3 | # shell scripts 4 | - repo: https://github.com/detailyang/pre-commit-shell 5 | rev: 1.0.5 6 | hooks: 7 | - id: shell-lint 8 | args: [ --format=json ] 9 | 10 | - repo: https://github.com/pre-commit/pre-commit-hooks 11 | rev: v4.5.0 12 | hooks: 13 | - id: check-added-large-files 14 | # check for unresolved merge conflicts 15 | - id: check-merge-conflict 16 | - id: check-shebang-scripts-are-executable 17 | - id: detect-private-key 18 | - id: end-of-file-fixer 19 | - id: mixed-line-ending 20 | - id: trailing-whitespace 21 | 22 | - repo: https://github.com/dnephin/pre-commit-golang 23 | rev: v0.5.1 24 | hooks: 25 | - id: go-fmt 26 | - id: go-mod-tidy 27 | - id: golangci-lint 28 | # timeout is needed for CI 29 | args: [ -E, gosec, -E, goconst, -E, govet, --timeout, 300s ] 30 | - id: go-imports 31 | args: [ -local, github.com/giantswarm/cert-operator ] 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [3.4.0] - 2024-03-28 11 | 12 | ### Changed 13 | 14 | - Avoid exiting with a failure at startup time if the PKI cleanup fails. 15 | 16 | ## [3.3.0] - 2024-03-26 17 | 18 | ### Added 19 | 20 | - Add team label in resources. 21 | - Add `global.podSecurityStandards.enforced` value for PSS migration. 22 | 23 | ### Changed 24 | 25 | - Configure `gsoci.azurecr.io` as the default container image registry. 26 | 27 | ## [3.2.1] - 2023-08-03 28 | 29 | ### Fixed 30 | 31 | - Fix rule names of PolicyException. 32 | 33 | ## [3.2.0] - 2023-07-17 34 | 35 | ### Fixed 36 | 37 | - Expand policy expception to cover old deployments. 38 | 39 | ## [3.1.0] - 2023-07-11 40 | 41 | ### Added 42 | 43 | - Added the use of the runtime/default seccomp profile. 44 | - Added Service Monitor. 45 | - Added required values for pss policies. 46 | - Added pss exceptions for volumes. 47 | 48 | ## [3.0.1] - 2022-11-29 49 | 50 | ### Fixed 51 | 52 | - Allow running unique and non unique cert-operators in the same namespace. 53 | 54 | ## [3.0.0] - 2022-11-23 55 | 56 | ### Added 57 | 58 | - Add possibility to run cert-operator as a unique app, reconciling special version '0.0.0'. 59 | 60 | ### Fixed 61 | 62 | - Avoid including certconfig UID in organizations for kubeconfig requests. 63 | 64 | ## [2.0.1] - 2022-04-04 65 | 66 | ### Fixed 67 | 68 | - Bump go module major version. 69 | 70 | ## [2.0.0] - 2022-03-31 71 | 72 | ### Changed 73 | 74 | - Use v1beta1 CAPI CRDs. 75 | - Bump `giantswarm/apiextensions` to `v6.0.0`. 76 | - Bump `giantswarm/exporterkit` to `v1.0.0`. 77 | - Bump `giantswarm/microendpoint` to `v1.0.0`. 78 | - Bump `giantswarm/microerror` to `v0.4.0`. 79 | - Bump `giantswarm/microkit` to `v1.0.0`. 80 | - Bump `giantswarm/micrologger` to `v0.6.0`. 81 | - Bump `giantswarm/k8sclient` to `v7.0.1`. 82 | - Bump `giantswarm/operatorkit` to `v7.0.1`. 83 | - Bump k8s dependencies to `v0.22.2`. 84 | - Bump `controller-runtime` to `v0.10.3`. 85 | 86 | ## [1.3.0] - 2022-01-03 87 | 88 | ### Changed 89 | 90 | - Use `RenewSelf` instead of `LookupSelf` to prevent expiration of `Vault token`. 91 | 92 | ## [1.2.0] - 2021-10-15 93 | 94 | ### Changed 95 | 96 | - Introducing `v1alpha3` CR's. 97 | 98 | ### Added 99 | 100 | - Add check to ensure that the `Cluster` resource is in the same namespace as the `certConfig` before creating the secret there. 101 | 102 | ## [1.1.0] - 2021-09-28 103 | 104 | ### Changed 105 | 106 | - Adjust helm chart to be used with `config-controller`. 107 | - Replace `jwt-go` with `golang-jwt/jwt`. 108 | - Manage Secrets in the same namespace in which CertConfigs are found. 109 | - Make `expirationThreshold` configurable. 110 | 111 | ## [1.0.1] - 2021-02-23 112 | 113 | ### Fixed 114 | 115 | - Add `list` permission for `cluster.x-k8s.io`. 116 | 117 | ## [1.0.0] - 2021-02-23 118 | 119 | ### Changed 120 | 121 | - Update Kubernetes dependencies to 1.18 versions. 122 | - Reconcile `CertConfig`s based on their `cert-operator.giantswarm.io/version` label. 123 | 124 | ### Removed 125 | 126 | - Stop using the `VersionBundle` version. 127 | 128 | ### Added 129 | 130 | - Add network policy resource. 131 | - Added lookup for nodepool clusters in other namespaces than `default`. 132 | 133 | ## [0.1.0-2] - 2020-08-11 134 | 135 | ### Fixed 136 | 137 | - Skip validation of reference versions like `0.1.0-1`. 138 | - Continue to export vault token expiration time as 0 when lookup fails. 139 | 140 | ### Changed 141 | 142 | - Update `apiextensions` to `0.4.1` version. 143 | - Set version `0.1.0` in `project.go`. 144 | - Use `architect` `2.1.2` in github actions. 145 | 146 | ## [0.1.0-1] - 2020-08-07 147 | 148 | ### Added 149 | 150 | - Add `k8s-jwt-to-vault-token` init container to ensure *vault* token secret exists. 151 | - Add Github automation workflows. 152 | 153 | ## [0.1.0] 2020-05-15 154 | 155 | ### Changed 156 | 157 | - No longer ensure CertConfig CRD. 158 | - Use architect-orb to release cert-operator. 159 | 160 | ### Added 161 | 162 | - First release. 163 | 164 | [Unreleased]: https://github.com/giantswarm/cert-operator/compare/v3.4.0...HEAD 165 | [3.4.0]: https://github.com/giantswarm/cert-operator/compare/v3.3.0...v3.4.0 166 | [3.3.0]: https://github.com/giantswarm/cert-operator/compare/v3.2.1...v3.3.0 167 | [3.2.1]: https://github.com/giantswarm/cert-operator/compare/v3.2.0...v3.2.1 168 | [3.2.0]: https://github.com/giantswarm/cert-operator/compare/v3.1.0...v3.2.0 169 | [3.1.0]: https://github.com/giantswarm/cert-operator/compare/v3.0.1...v3.1.0 170 | [3.0.1]: https://github.com/giantswarm/cert-operator/compare/v3.0.0...v3.0.1 171 | [3.0.0]: https://github.com/giantswarm/cert-operator/compare/v2.0.1...v3.0.0 172 | [2.0.1]: https://github.com/giantswarm/cert-operator/compare/v2.0.0...v2.0.1 173 | [2.0.0]: https://github.com/giantswarm/cert-operator/compare/v1.3.0...v2.0.0 174 | [1.3.0]: https://github.com/giantswarm/cert-operator/compare/v1.2.0...v1.3.0 175 | [1.2.0]: https://github.com/giantswarm/cert-operator/compare/v1.1.0...v1.2.0 176 | [1.1.0]: https://github.com/giantswarm/cert-operator/compare/v1.0.1...v1.1.0 177 | [1.0.1]: https://github.com/giantswarm/cert-operator/compare/v1.0.0...v1.0.1 178 | [1.0.0]: https://github.com/giantswarm/cert-operator/compare/v0.1.0-2...v1.0.0 179 | [0.1.0-2]: https://github.com/giantswarm/cert-operator/compare/v0.1.0-1...v0.1.0-2 180 | [0.1.0-1]: https://github.com/giantswarm/cert-operator/compare/v0.1.0...v0.1.0-1 181 | [0.1.0]: https://github.com/giantswarm/cert-operator/releases/tag/v0.1.0 182 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # generated by giantswarm/github actions - changes will be overwritten 2 | * @giantswarm/team-phoenix 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | cert-operator is Apache 2.0 licensed and accepts contributions via GitHub pull requests. This document outlines some of the conventions on commit message formatting, contact points for developers and other resources to make getting your contribution into cert-operator easier. 4 | 5 | # Email and chat 6 | 7 | - Email: [giantswarm](https://groups.google.com/forum/#!forum/giantswarm) 8 | - IRC: #[giantswarm](irc://irc.freenode.org:6667/#giantswarm) IRC channel on freenode.org 9 | 10 | ## Getting started 11 | 12 | - Fork the repository on GitHub 13 | - Read the [README](README.md) for build instructions 14 | 15 | ## Reporting Bugs and Creating Issues 16 | 17 | Reporting bugs is one of the best ways to contribute. If you find bugs or documentation mistakes in the cert-operator project, please let us know by [opening an issue](https://github.com/giantswarm/cert-operator/issues/new). We treat bugs and mistakes very seriously and believe no issue is too small. Before creating a bug report, please check there that one does not already exist. 18 | 19 | To make your bug report accurate and easy to understand, please try to create bug reports that are: 20 | 21 | - Specific. Include as much details as possible: which version, what environment, what configuration, etc. You can also attach logs. 22 | 23 | - Reproducible. Include the steps to reproduce the problem. We understand some issues might be hard to reproduce, please includes the steps that might lead to the problem. If applicable, you can also attach affected data dir(s) and a stack trace to the bug report. 24 | 25 | - Isolated. Please try to isolate and reproduce the bug with minimum dependencies. It would significantly slow down the speed to fix a bug if too many dependencies are involved in a bug report. Debugging external systems that rely on cert-operator is out of scope, but we are happy to point you in the right direction or help you interact with cert-operator in the correct manner. 26 | 27 | - Unique. Do not duplicate existing bug reports. 28 | 29 | - Scoped. One bug per report. Do not follow up with another bug inside one report. 30 | 31 | You might also want to read [Elika Etemad’s article on filing good bug reports](http://fantasai.inkedblade.net/style/talks/filing-good-bugs/) before creating a bug report. 32 | 33 | We might ask you for further information to locate a bug. A duplicated bug report will be closed. 34 | 35 | ## Contribution flow 36 | 37 | This is a rough outline of what a contributor's workflow looks like: 38 | 39 | - Create a feature branch from where you want to base your work. This is usually master. 40 | - Make commits of logical units. 41 | - Make sure your commit messages are in the proper format (see below). 42 | - Push your changes to a topic branch in your fork of the repository. 43 | - Submit a pull request to giantswarm/cert-operator. 44 | - Adding unit tests will greatly improve the chance for getting a quick review and your PR accepted. 45 | - Your PR must receive a LGTM from one maintainer found in the MAINTAINERS file. 46 | - Before merging your PR be sure to squash all commits into one. 47 | 48 | Thanks for your contributions! 49 | 50 | ### Code style 51 | 52 | The coding style suggested by the Golang community is used. See the [style doc](https://github.com/golang/go/wiki/CodeReviewComments) for details. 53 | 54 | Please follow this style to make the code easy to review, maintain, and develop. 55 | 56 | ### Format of the Commit Message 57 | 58 | We follow a rough convention for commit messages that is designed to answer two 59 | questions: what changed and why. The subject line should feature the what and 60 | the body of the commit should describe the why. 61 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 660 York Street, Suite 102, 6 | San Francisco, CA 94110 USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | 12 | Developer's Certificate of Origin 1.1 13 | 14 | By making a contribution to this project, I certify that: 15 | 16 | (a) The contribution was created in whole or in part by me and I 17 | have the right to submit it under the open source license 18 | indicated in the file; or 19 | 20 | (b) The contribution is based upon previous work that, to the best 21 | of my knowledge, is covered under an appropriate open source 22 | license and I have the right under that license to submit that 23 | work with modifications, whether created in whole or in part 24 | by me, under the same open source license (unless I am 25 | permitted to submit under a different license), as indicated 26 | in the file; or 27 | 28 | (c) The contribution was provided directly to me by some other 29 | person who certified (a), (b) or (c) and I have not modified 30 | it. 31 | 32 | (d) I understand and agree that this project and the contribution 33 | are public and that a record of the contribution (including all 34 | personal information I submit with it, including my sign-off) is 35 | maintained indefinitely and may be redistributed consistent with 36 | this project or the open source license(s) involved. 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.19.1 2 | 3 | RUN apk add --no-cache ca-certificates 4 | 5 | ADD ./cert-operator /cert-operator 6 | 7 | ENTRYPOINT ["/cert-operator"] 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl@6.23.3 4 | # 5 | 6 | include Makefile.*.mk 7 | 8 | ##@ General 9 | 10 | # The help target prints out all targets with their descriptions organized 11 | # beneath their categories. The categories are represented by '##@' and the 12 | # target descriptions by '##'. The awk commands is responsible for reading the 13 | # entire set of makefiles included in this invocation, looking for lines of the 14 | # file as xyz: ## something, and then pretty-format the target and help. Then, 15 | # if there's a line with ##@ something, that gets pretty-printed as a category. 16 | # More info on the usage of ANSI control characters for terminal formatting: 17 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 18 | # More info on the awk command: 19 | # http://linuxcommand.org/lc3_adv_awk.php 20 | 21 | .PHONY: help 22 | help: ## Display this help. 23 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z%\\\/_0-9-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 24 | -------------------------------------------------------------------------------- /Makefile.gen.app.mk: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl@6.23.3 4 | # 5 | 6 | ##@ App 7 | 8 | YQ=docker run --rm -u $$(id -u) -v $${PWD}:/workdir mikefarah/yq:4.29.2 9 | HELM_DOCS=docker run --rm -u $$(id -u) -v $${PWD}:/helm-docs jnorwood/helm-docs:v1.11.0 10 | 11 | ifdef APPLICATION 12 | DEPS := $(shell find $(APPLICATION)/charts -maxdepth 2 -name "Chart.yaml" -printf "%h\n") 13 | endif 14 | 15 | .PHONY: lint-chart check-env update-chart helm-docs update-deps $(DEPS) 16 | 17 | lint-chart: IMAGE := giantswarm/helm-chart-testing:v3.0.0-rc.1 18 | lint-chart: check-env ## Runs ct against the default chart. 19 | @echo "====> $@" 20 | rm -rf /tmp/$(APPLICATION)-test 21 | mkdir -p /tmp/$(APPLICATION)-test/helm 22 | cp -a ./helm/$(APPLICATION) /tmp/$(APPLICATION)-test/helm/ 23 | architect helm template --dir /tmp/$(APPLICATION)-test/helm/$(APPLICATION) 24 | docker run -it --rm -v /tmp/$(APPLICATION)-test:/wd --workdir=/wd --name ct $(IMAGE) ct lint --validate-maintainers=false --charts="helm/$(APPLICATION)" 25 | rm -rf /tmp/$(APPLICATION)-test 26 | 27 | update-chart: check-env ## Sync chart with upstream repo. 28 | @echo "====> $@" 29 | vendir sync 30 | $(MAKE) update-deps 31 | 32 | update-deps: check-env $(DEPS) ## Update Helm dependencies. 33 | cd $(APPLICATION) && helm dependency update 34 | 35 | $(DEPS): check-env ## Update main Chart.yaml with new local dep versions. 36 | dep_name=$(shell basename $@) && \ 37 | new_version=`$(YQ) .version $(APPLICATION)/charts/$$dep_name/Chart.yaml` && \ 38 | $(YQ) -i e "with(.dependencies[]; select(.name == \"$$dep_name\") | .version = \"$$new_version\")" $(APPLICATION)/Chart.yaml 39 | 40 | helm-docs: check-env ## Update $(APPLICATION) README. 41 | $(HELM_DOCS) -c $(APPLICATION) -g $(APPLICATION) 42 | 43 | check-env: 44 | ifndef APPLICATION 45 | $(error APPLICATION is not defined) 46 | endif 47 | -------------------------------------------------------------------------------- /Makefile.gen.go.mk: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl@6.23.3 4 | # 5 | 6 | APPLICATION := $(shell go list -m | cut -d '/' -f 3) 7 | BUILDTIMESTAMP := $(shell date -u '+%FT%TZ') 8 | GITSHA1 := $(shell git rev-parse --verify HEAD) 9 | MODULE := $(shell go list -m) 10 | OS := $(shell go env GOOS) 11 | SOURCES := $(shell find . -name '*.go') 12 | VERSION := $(shell architect project version) 13 | ifeq ($(OS), linux) 14 | EXTLDFLAGS := -static 15 | endif 16 | LDFLAGS ?= -w -linkmode 'auto' -extldflags '$(EXTLDFLAGS)' \ 17 | -X '$(shell go list -m)/pkg/project.buildTimestamp=${BUILDTIMESTAMP}' \ 18 | -X '$(shell go list -m)/pkg/project.gitSHA=${GITSHA1}' 19 | 20 | .DEFAULT_GOAL := build 21 | 22 | ##@ Go 23 | 24 | .PHONY: build build-darwin build-darwin-64 build-linux build-linux-arm64 build-windows-amd64 25 | build: $(APPLICATION) ## Builds a local binary. 26 | @echo "====> $@" 27 | build-darwin: $(APPLICATION)-darwin ## Builds a local binary for darwin/amd64. 28 | @echo "====> $@" 29 | build-darwin-arm64: $(APPLICATION)-darwin-arm64 ## Builds a local binary for darwin/arm64. 30 | @echo "====> $@" 31 | build-linux: $(APPLICATION)-linux ## Builds a local binary for linux/amd64. 32 | @echo "====> $@" 33 | build-linux-arm64: $(APPLICATION)-linux-arm64 ## Builds a local binary for linux/arm64. 34 | @echo "====> $@" 35 | build-windows-amd64: $(APPLICATION)-windows-amd64.exe ## Builds a local binary for windows/amd64. 36 | @echo "====> $@" 37 | 38 | $(APPLICATION): $(APPLICATION)-v$(VERSION)-$(OS)-amd64 39 | @echo "====> $@" 40 | cp -a $< $@ 41 | 42 | $(APPLICATION)-darwin: $(APPLICATION)-v$(VERSION)-darwin-amd64 43 | @echo "====> $@" 44 | cp -a $< $@ 45 | 46 | $(APPLICATION)-darwin-arm64: $(APPLICATION)-v$(VERSION)-darwin-arm64 47 | @echo "====> $@" 48 | cp -a $< $@ 49 | 50 | $(APPLICATION)-linux: $(APPLICATION)-v$(VERSION)-linux-amd64 51 | @echo "====> $@" 52 | cp -a $< $@ 53 | 54 | $(APPLICATION)-linux-arm64: $(APPLICATION)-v$(VERSION)-linux-arm64 55 | @echo "====> $@" 56 | cp -a $< $@ 57 | 58 | $(APPLICATION)-windows-amd64.exe: $(APPLICATION)-v$(VERSION)-windows-amd64.exe 59 | @echo "====> $@" 60 | cp -a $< $@ 61 | 62 | $(APPLICATION)-v$(VERSION)-%-amd64: $(SOURCES) 63 | @echo "====> $@" 64 | CGO_ENABLED=0 GOOS=$* GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o $@ . 65 | 66 | $(APPLICATION)-v$(VERSION)-%-arm64: $(SOURCES) 67 | @echo "====> $@" 68 | CGO_ENABLED=0 GOOS=$* GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o $@ . 69 | 70 | $(APPLICATION)-v$(VERSION)-windows-amd64.exe: $(SOURCES) 71 | @echo "====> $@" 72 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o $@ . 73 | 74 | .PHONY: install 75 | install: ## Install the application. 76 | @echo "====> $@" 77 | go install -ldflags "$(LDFLAGS)" . 78 | 79 | .PHONY: run 80 | run: ## Runs go run main.go. 81 | @echo "====> $@" 82 | go run -ldflags "$(LDFLAGS)" -race . 83 | 84 | .PHONY: clean 85 | clean: ## Cleans the binary. 86 | @echo "====> $@" 87 | rm -f $(APPLICATION)* 88 | go clean 89 | 90 | .PHONY: imports 91 | imports: ## Runs goimports. 92 | @echo "====> $@" 93 | goimports -local $(MODULE) -w . 94 | 95 | .PHONY: lint 96 | lint: ## Runs golangci-lint. 97 | @echo "====> $@" 98 | golangci-lint run -E gosec -E goconst --timeout=15m ./... 99 | 100 | .PHONY: nancy 101 | nancy: ## Runs nancy (requires v1.0.37 or newer). 102 | @echo "====> $@" 103 | CGO_ENABLED=0 go list -json -deps ./... | nancy sleuth --skip-update-check --quiet --exclude-vulnerability-file ./.nancy-ignore --additional-exclude-vulnerability-files ./.nancy-ignore.generated 104 | 105 | .PHONY: test 106 | test: ## Runs go test with default values. 107 | @echo "====> $@" 108 | go test -ldflags "$(LDFLAGS)" -race ./... 109 | 110 | .PHONY: build-docker 111 | build-docker: build-linux ## Builds docker image to registry. 112 | @echo "====> $@" 113 | cp -a $(APPLICATION)-linux $(APPLICATION) 114 | docker build -t ${APPLICATION}:${VERSION} . 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/giantswarm/cert-operator/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/giantswarm/cert-operator/tree/master) 2 | 3 | # cert-operator 4 | 5 | Cert Operator creates, configures, and manages certificates for Kubernetes clusters 6 | running on the Giant Swarm platform. 7 | 8 | Most of the functionality currently provided by this project is now supported natively by Kubernetes' Cluster API (CAPI). As we move more platform functionality to use CAPI workflows, this project will eventually be deprecated. 9 | 10 | ## About 11 | 12 | `cert-operator` is responsible for provisioning certificates used by components of the Giant Swarm platform. It reconciles [`CertConfig` Custom Resources](https://docs.giantswarm.io/ui-api/management-api/crd/certconfigs.core.giantswarm.io/) (CRs) and configures Hashicorp `vault` accordingly. For a given `CertConfig`, `cert-operator` ensures: 13 | 14 | - `vault` is accessible 15 | - the necessary `vault` PKI backend has been created 16 | - a root CA for the associated workload cluster has been created using the PKI backend 17 | 18 | Secrets are then created in the management cluster containing the certificates, signed by the root CA, used for establishing connections with and within the workload cluster. 19 | Currently, `cert-operator` handles creation of kubeconfigs for workload cluster access for the following components: 20 | 21 | - the Giant Swarm API 22 | - app-operator 23 | - aws/azure/kvm-operator 24 | - calico 25 | - etcd 26 | - node-operator 27 | - Prometheus 28 | 29 | ### Compatibility 30 | 31 | | provider | cert-operator | cluster-operator | 32 | | -----------|:---------:|:---------:| 33 | | AWS | < 1.0.0* | < 3.6.1 | 34 | | AWS | >= 1.0.1 | >= 3.6.1 | 35 | | all others | >= 1.0.1 | >= 0.24.1 | 36 | | all others | < 1.0.1 | < 0.24.0* | 37 | 38 | \* cert-operator v1.0.0 and cluster-operator v0.24.0 have known issues. Use v1.0.1 or v0.24.1 instead. 39 | 40 | Prior to version 1.0.0, `cert-operator` reconciled based on the `spec.versionBundle.version` field of the `CertConfig` CR. 41 | 42 | In version 1.0.0 and later, the CR field is ignored, and the operator reconciles `CertConfig`s which have the `cert-operator.giantswarm.io/version` label set to the operator's version. 43 | 44 | In a typical pre-CAPI Giant Swarm release, `cluster-operator` creates the `CertConfig`s necessary for each cluster. `cluster-operator` prior to version 3.6.1 (AWS) and 0.24.0 (Azure and KVM) did not set the appropriate label and still used the older hardcoded `versionBundle`. The two methods are not compatible. 45 | 46 | ## Prerequisites 47 | 48 | ## Getting Project 49 | 50 | Download the latest release: 51 | https://github.com/giantswarm/cert-operator/releases/latest 52 | 53 | Clone the git repository: https://github.com/giantswarm/cert-operator.git 54 | 55 | Download the latest docker image from here: 56 | https://quay.io/repository/giantswarm/cert-operator 57 | 58 | 59 | ### How to build 60 | 61 | 62 | #### Dependencies 63 | 64 | - [github.com/giantswarm/microkit](https://github.com/giantswarm/microkit) 65 | 66 | 67 | #### Building the standard way 68 | 69 | ``` 70 | go build github.com/giantswarm/cert-operator 71 | ``` 72 | 73 | 74 | ## Running cert-operator 75 | 76 | See [this guide][examples-local]. 77 | 78 | [examples-local]: https://github.com/giantswarm/cert-operator/blob/master/examples/README.md 79 | 80 | 81 | ## Contact 82 | 83 | - Mailing list: [giantswarm](https://groups.google.com/forum/!forum/giantswarm) 84 | - IRC: #[giantswarm](irc://irc.freenode.org:6667/#giantswarm) on freenode.org 85 | - Bugs: [issues](https://github.com/giantswarm/cert-operator/issues) 86 | 87 | 88 | ## Contributing & Reporting Bugs 89 | 90 | See [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches, the 91 | contribution workflow as well as reporting bugs. 92 | 93 | 94 | ## License 95 | 96 | cert-operator is under the Apache 2.0 license. See the [LICENSE](LICENSE) file 97 | for details. 98 | 99 | 100 | ## Credit 101 | - https://golang.org 102 | - https://github.com/giantswarm/microkit 103 | 104 | 105 | ### Secrets 106 | 107 | The cert-operator is deployed via Kubernetes. 108 | 109 | Here the plain Vault token has to be inserted. 110 | 111 | ``` 112 | service: 113 | vault: 114 | config: 115 | token: 'TODO' 116 | ``` 117 | 118 | Here the base64 representation of the data structure above has to be inserted. 119 | 120 | ``` 121 | apiVersion: v1 122 | kind: Secret 123 | metadata: 124 | name: cert-operator-secret 125 | namespace: giantswarm 126 | type: Opaque 127 | data: 128 | secret.yaml: 'TODO' 129 | ``` 130 | 131 | To create the secret manually do this. 132 | 133 | ``` 134 | kubectl create -f ./path/to/secret.yaml 135 | ``` 136 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please visit for information on reporting security issues. 6 | -------------------------------------------------------------------------------- /client/vault/error.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "github.com/giantswarm/microerror" 5 | ) 6 | 7 | var invalidConfigError = µerror.Error{ 8 | Kind: "invalidConfigError", 9 | } 10 | 11 | // IsInvalidConfig asserts invalidConfigError. 12 | func IsInvalidConfig(err error) bool { 13 | return microerror.Cause(err) == invalidConfigError 14 | } 15 | -------------------------------------------------------------------------------- /client/vault/vault.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "net/url" 5 | 6 | "github.com/giantswarm/microerror" 7 | vaultapi "github.com/hashicorp/vault/api" 8 | "github.com/spf13/viper" 9 | 10 | "github.com/giantswarm/cert-operator/v3/flag" 11 | ) 12 | 13 | type Config struct { 14 | // Settings. 15 | Flag *flag.Flag 16 | Viper *viper.Viper 17 | } 18 | 19 | func NewClient(config Config) (*vaultapi.Client, error) { 20 | address := config.Viper.GetString(config.Flag.Service.Vault.Config.Address) 21 | token := config.Viper.GetString(config.Flag.Service.Vault.Config.Token) 22 | 23 | if address == "" { 24 | return nil, microerror.Maskf(invalidConfigError, "vault address must not be empty") 25 | } 26 | 27 | // Check Vault address is valid. 28 | _, err := url.ParseRequestURI(address) 29 | if err != nil { 30 | return nil, microerror.Mask(err) 31 | } 32 | 33 | if token == "" { 34 | return nil, microerror.Maskf(invalidConfigError, "vault token must not be empty") 35 | } 36 | 37 | newClientConfig := vaultapi.DefaultConfig() 38 | newClientConfig.Address = address 39 | 40 | newVaultClient, err := vaultapi.NewClient(newClientConfig) 41 | if err != nil { 42 | return nil, err 43 | } 44 | newVaultClient.SetToken(token) 45 | 46 | return newVaultClient, nil 47 | } 48 | -------------------------------------------------------------------------------- /client/vault/vault_test.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/spf13/viper" 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/giantswarm/cert-operator/v3/flag" 11 | ) 12 | 13 | func TestNewClient(t *testing.T) { 14 | tests := []struct { 15 | name string 16 | address string 17 | token string 18 | expectedError bool 19 | }{ 20 | { 21 | name: "Specify vault address and token. It should return a vault client.", 22 | address: "http://localhost:8200", 23 | token: "auth-token", 24 | expectedError: false, 25 | }, 26 | { 27 | name: "Specify vault address but no token. It should return an error.", 28 | address: "http://localhost:8200", 29 | token: "", 30 | expectedError: true, 31 | }, 32 | { 33 | name: "Specify a vault token but no address. It should return an error.", 34 | address: "", 35 | token: "auth-token", 36 | expectedError: true, 37 | }, 38 | { 39 | name: "Specify an invalid vault address. It should return an error.", 40 | address: "http//invalid-address", 41 | token: "auth-token", 42 | expectedError: true, 43 | }, 44 | } 45 | 46 | for _, tc := range tests { 47 | f := flag.New() 48 | v := viper.New() 49 | 50 | v.Set(f.Service.Vault.Config.Address, tc.address) 51 | v.Set(f.Service.Vault.Config.Token, tc.token) 52 | 53 | config := Config{ 54 | Flag: f, 55 | Viper: v, 56 | } 57 | 58 | vaultClient, err := NewClient(config) 59 | if tc.expectedError { 60 | assert.Error(t, err, fmt.Sprintf("[%s] An error was expected", tc.name)) 61 | continue 62 | } else { 63 | assert.NotNil(t, vaultClient, "Vault client is nil but should not be") 64 | assert.Nil(t, err, "Unexpected error creating vault client") 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Running cert-operator Locally 2 | 3 | **Note:** This should only be used for testing and development. See the 4 | [/kubernetes/][kubernetes-dir] directory and [Secrets][secrets-doc] for 5 | a production ready configuration. 6 | 7 | [kubernetes-dir]: https://github.com/giantswarm/cert-operator/tree/master/kubernetes 8 | [secrests-doc]: https://github.com/giantswarm/cert-operator#secrets 9 | 10 | This guide explains how to get running cert-operator locally. For example on 11 | minikube. Certificates created here are meant to be used by [aws-operator]. 12 | 13 | All commands are assumed to be run from `examples/local` directory. 14 | 15 | [aws-operator]: https://github.com/giantswarm/aws-operator 16 | 17 | ## Cluster-Local Docker Image 18 | 19 | The operator needs a connection to the K8s API. The simplest approach is to run 20 | as a deployment and use the "in cluster" configuration. 21 | 22 | In that case the Docker image needs to be accessible from the K8s cluster 23 | running the operator. For Minikube `eval $(minikube docker-env)` before `docker 24 | build`, see [reusing the Docker daemon] for details. 25 | 26 | [reusing the docker daemon]: https://github.com/kubernetes/minikube/blob/master/docs/reusing_the_docker_daemon.md 27 | 28 | ```bash 29 | # Optional. Only when using Minikube. 30 | eval $(minikube docker-env) 31 | 32 | # From the root of the project, where the Dockerfile resides 33 | CGO_ENABLED=0 GOOS=linux go build github.com/giantswarm/cert-operator 34 | docker build -t quay.io/giantswarm/cert-operator:local-lab . 35 | 36 | # Optional. Restart running operator after image update. 37 | # Does nothing when the operator is not deployed. 38 | #kubectl delete pod -l app=cert-operator-local 39 | ``` 40 | 41 | ## Deploying the lab charts 42 | 43 | The lab consist of three Helm charts, `cert-operator-lab-chart`, which sets up cert-operator, 44 | `cert-resource-lab-chart`, which puts in place the required certificates and `vaultlab-chart`, 45 | which installs Vault in dev mode. For installing the latter two you need the [Helm registry plugin](https://github.com/app-registry/appr-helm-plugin) 46 | 47 | With a working Helm installation they can be created from the project's root with: 48 | 49 | ```bash 50 | $ helm registry install quay.io/giantswarm/vaultlab-chart:stable -- \ 51 | -n vault \ 52 | --set vaultToken=myToken 53 | 54 | $ helm install -n cert-operator-lab \ 55 | --set imageTag=local-lab \ 56 | --set vaultToken=myToken \ 57 | --set commonDomain=mydomain.io \ 58 | ./examples/cert-operator-lab-chart/ --wait 59 | 60 | helm registry install quay.io/giantswarm/cert-resource-lab-chart:stable -- \ 61 | -n cert-resource-lab \ 62 | --set commonDomain=mydomain.io \ 63 | --set clusterName=test-cluster 64 | ``` 65 | 66 | The certificates are issued using Vault and stored as K8s secrets. 67 | 68 | ```bash 69 | kubectl get secret -l clusterID=test-cluster # or the actual value of `clusterName` 70 | ``` 71 | 72 | `cert-operator-lab-chart` accepts the following configuration parameters: 73 | * `commonDomain` - Domain to be used by [aws-operator]. 74 | * `vaultHost` - Defaults to `vault` for the local setup. 75 | * `vaultToken` - It must match across the Vault service and the operator deployment flags. 76 | * `imageTag` - Tag of the cert-operator image to be used, by default `local-dev` to use a locally created 77 | image. 78 | 79 | `cert-resource-lab-chart` is also configurable with `clusterName` and `commonDomain` (the latter should match the value 80 | used in `cert-operator-lab-chart`). 81 | 82 | 83 | You can specify different values of the configuration parameters changing the `values.yaml` file on each 84 | chart directory or specifying them on the install command: 85 | ```bash 86 | $ helm install -n cert-operator-lab --set clusterName=my-cluste-name ./cert-operator-lab-chart/ --wait 87 | ``` 88 | 89 | ## Cleaning Up 90 | 91 | Delete the cert-operator and certificates lab releases: 92 | 93 | ```bash 94 | $ helm delete cert-resource-lab --purge 95 | $ helm delete cert-operator-lab --purge 96 | ``` 97 | -------------------------------------------------------------------------------- /examples/cert-operator-lab-chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: cert-operator-lab-chart 2 | version: 0.1.2 3 | description: cert-opertor local example chart 4 | -------------------------------------------------------------------------------- /examples/cert-operator-lab-chart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: cert-operator-local 5 | namespace: default 6 | labels: 7 | app: cert-operator-local 8 | spec: 9 | replicas: 1 10 | strategy: 11 | type: RollingUpdate 12 | template: 13 | metadata: 14 | labels: 15 | app: cert-operator-local 16 | spec: 17 | volumes: 18 | containers: 19 | - name: cert-operator 20 | image: quay.io/giantswarm/cert-operator:{{.Values.imageTag}} 21 | imagePullPolicy: IfNotPresent 22 | ports: 23 | - name: http 24 | containerPort: 8000 25 | args: 26 | - daemon 27 | - --service.vault.config.address=http://{{.Values.vaultHost}}:8200 28 | - --service.vault.config.token={{.Values.vaultToken}} 29 | - --service.vault.config.pki.ca.ttl=1440h 30 | - --service.vault.config.pki.commonname.format=%s.{{.Values.commonDomain}} 31 | - --service.kubernetes.incluster=true 32 | - --service.resource.vaultcrt.expirationthreshold=24h 33 | - --service.resource.vaultcrt.namespace=default 34 | -------------------------------------------------------------------------------- /examples/cert-operator-lab-chart/values.yaml: -------------------------------------------------------------------------------- 1 | commonDomain: "aws.gigantic.io" 2 | vaultToken: "myToken" 3 | vaultHost: "vault" 4 | imageTag: "local-lab" 5 | -------------------------------------------------------------------------------- /flag/flag.go: -------------------------------------------------------------------------------- 1 | package flag 2 | 3 | import ( 4 | "github.com/giantswarm/microkit/flag" 5 | 6 | "github.com/giantswarm/cert-operator/v3/flag/service" 7 | ) 8 | 9 | type Flag struct { 10 | Service service.Service 11 | } 12 | 13 | func New() *Flag { 14 | f := &Flag{} 15 | flag.Init(f) 16 | return f 17 | } 18 | -------------------------------------------------------------------------------- /flag/service/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | type App struct { 4 | Unique string 5 | } 6 | -------------------------------------------------------------------------------- /flag/service/crd/crd.go: -------------------------------------------------------------------------------- 1 | package crd 2 | 3 | type CRD struct { 4 | LabelSelector string 5 | } 6 | -------------------------------------------------------------------------------- /flag/service/resource/resource.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/giantswarm/cert-operator/v3/flag/service/resource/vaultcrt" 5 | ) 6 | 7 | type Resource struct { 8 | VaultCrt vaultcrt.VaultCrt 9 | } 10 | -------------------------------------------------------------------------------- /flag/service/resource/vaultcrt/vaultrt.go: -------------------------------------------------------------------------------- 1 | package vaultcrt 2 | 3 | type VaultCrt struct { 4 | ExpirationThreshold string 5 | Namespace string 6 | } 7 | -------------------------------------------------------------------------------- /flag/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/giantswarm/operatorkit/v7/pkg/flag/service/kubernetes" 5 | 6 | "github.com/giantswarm/cert-operator/v3/flag/service/app" 7 | "github.com/giantswarm/cert-operator/v3/flag/service/crd" 8 | "github.com/giantswarm/cert-operator/v3/flag/service/resource" 9 | "github.com/giantswarm/cert-operator/v3/flag/service/vault" 10 | ) 11 | 12 | type Service struct { 13 | App app.App 14 | CRD crd.CRD 15 | Kubernetes kubernetes.Kubernetes 16 | Resource resource.Resource 17 | Vault vault.Vault 18 | } 19 | -------------------------------------------------------------------------------- /flag/service/vault/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/giantswarm/cert-operator/v3/flag/service/vault/config/pki" 5 | ) 6 | 7 | type Config struct { 8 | Address string 9 | Token string 10 | 11 | PKI pki.PKI 12 | } 13 | -------------------------------------------------------------------------------- /flag/service/vault/config/pki/ca/ca.go: -------------------------------------------------------------------------------- 1 | package ca 2 | 3 | type CA struct { 4 | TTL string 5 | } 6 | -------------------------------------------------------------------------------- /flag/service/vault/config/pki/commonname/commonname.go: -------------------------------------------------------------------------------- 1 | package commonname 2 | 3 | type CommonName struct { 4 | Format string 5 | } 6 | -------------------------------------------------------------------------------- /flag/service/vault/config/pki/pki.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "github.com/giantswarm/cert-operator/v3/flag/service/vault/config/pki/ca" 5 | "github.com/giantswarm/cert-operator/v3/flag/service/vault/config/pki/commonname" 6 | ) 7 | 8 | type PKI struct { 9 | CA ca.CA 10 | CommonName commonname.CommonName 11 | } 12 | -------------------------------------------------------------------------------- /flag/service/vault/vault.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "github.com/giantswarm/cert-operator/v3/flag/service/vault/config" 5 | ) 6 | 7 | type Vault struct { 8 | Config config.Config 9 | } 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/giantswarm/cert-operator/v3 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/giantswarm/apiextensions/v6 v6.5.0 7 | github.com/giantswarm/certs/v4 v4.0.0 8 | github.com/giantswarm/exporterkit v1.0.0 9 | github.com/giantswarm/k8sclient/v7 v7.0.1 10 | github.com/giantswarm/microendpoint v1.0.0 11 | github.com/giantswarm/microerror v0.4.0 12 | github.com/giantswarm/microkit v1.0.0 13 | github.com/giantswarm/micrologger v1.0.0 14 | github.com/giantswarm/operatorkit/v7 v7.0.0 15 | github.com/giantswarm/vaultcrt v0.2.0 16 | github.com/giantswarm/vaultpki v0.2.0 17 | github.com/giantswarm/vaultrole v0.2.0 18 | github.com/hashicorp/vault/api v1.12.2 19 | github.com/prometheus/client_golang v1.19.0 20 | github.com/spf13/viper v1.18.2 21 | github.com/stretchr/testify v1.9.0 22 | k8s.io/api v0.25.4 23 | k8s.io/apimachinery v0.25.4 24 | k8s.io/client-go v0.25.4 25 | sigs.k8s.io/cluster-api v1.2.6 26 | sigs.k8s.io/controller-runtime v0.13.1 27 | ) 28 | 29 | require ( 30 | github.com/beorn7/perks v1.0.1 // indirect 31 | github.com/blang/semver v3.5.1+incompatible // indirect 32 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect 33 | github.com/cenkalti/backoff/v4 v4.2.0 // indirect 34 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 35 | github.com/coreos/go-semver v0.3.0 // indirect 36 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 37 | github.com/emicklei/go-restful/v3 v3.10.1 // indirect 38 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 39 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect 40 | github.com/fsnotify/fsnotify v1.7.0 // indirect 41 | github.com/getsentry/sentry-go v0.15.0 // indirect 42 | github.com/giantswarm/backoff v1.0.0 // indirect 43 | github.com/giantswarm/to v0.4.0 // indirect 44 | github.com/giantswarm/versionbundle v1.0.0 // indirect 45 | github.com/go-jose/go-jose/v3 v3.0.3 // indirect 46 | github.com/go-kit/kit v0.12.0 // indirect 47 | github.com/go-kit/log v0.2.1 // indirect 48 | github.com/go-logfmt/logfmt v0.5.1 // indirect 49 | github.com/go-logr/logr v1.2.3 // indirect 50 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 51 | github.com/go-openapi/jsonreference v0.20.0 // indirect 52 | github.com/go-openapi/swag v0.22.3 // indirect 53 | github.com/go-stack/stack v1.8.1 // indirect 54 | github.com/gogo/protobuf v1.3.2 // indirect 55 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 56 | github.com/golang/protobuf v1.5.3 // indirect 57 | github.com/google/gnostic v0.6.9 // indirect 58 | github.com/google/go-cmp v0.6.0 // indirect 59 | github.com/google/gofuzz v1.2.0 // indirect 60 | github.com/google/uuid v1.4.0 // indirect 61 | github.com/gorilla/mux v1.8.0 // indirect 62 | github.com/hashicorp/errwrap v1.1.0 // indirect 63 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 64 | github.com/hashicorp/go-multierror v1.1.1 // indirect 65 | github.com/hashicorp/go-retryablehttp v0.7.1 // indirect 66 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 67 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect 68 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 69 | github.com/hashicorp/go-sockaddr v1.0.2 // indirect 70 | github.com/hashicorp/hcl v1.0.0 // indirect 71 | github.com/hashicorp/vault/sdk v0.6.1 // indirect 72 | github.com/imdario/mergo v0.3.13 // indirect 73 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 74 | github.com/josharian/intern v1.0.0 // indirect 75 | github.com/json-iterator/go v1.1.12 // indirect 76 | github.com/magiconair/properties v1.8.7 // indirect 77 | github.com/mailru/easyjson v0.7.7 // indirect 78 | github.com/mitchellh/go-homedir v1.1.0 // indirect 79 | github.com/mitchellh/mapstructure v1.5.0 // indirect 80 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 81 | github.com/modern-go/reflect2 v1.0.2 // indirect 82 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 83 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 84 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 85 | github.com/pkg/errors v0.9.1 // indirect 86 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 87 | github.com/prometheus/client_model v0.5.0 // indirect 88 | github.com/prometheus/common v0.48.0 // indirect 89 | github.com/prometheus/procfs v0.12.0 // indirect 90 | github.com/ryanuber/go-glob v1.0.0 // indirect 91 | github.com/sagikazarmark/locafero v0.4.0 // indirect 92 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 93 | github.com/sourcegraph/conc v0.3.0 // indirect 94 | github.com/spf13/afero v1.11.0 // indirect 95 | github.com/spf13/cast v1.6.0 // indirect 96 | github.com/spf13/cobra v1.6.1 // indirect 97 | github.com/spf13/pflag v1.0.5 // indirect 98 | github.com/subosito/gotenv v1.6.0 // indirect 99 | go.uber.org/atomic v1.10.0 // indirect 100 | go.uber.org/multierr v1.9.0 // indirect 101 | golang.org/x/crypto v0.19.0 // indirect 102 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 103 | golang.org/x/net v0.20.0 // indirect 104 | golang.org/x/oauth2 v0.16.0 // indirect 105 | golang.org/x/sync v0.5.0 // indirect 106 | golang.org/x/sys v0.17.0 // indirect 107 | golang.org/x/term v0.17.0 // indirect 108 | golang.org/x/text v0.14.0 // indirect 109 | golang.org/x/time v0.5.0 // indirect 110 | gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect 111 | google.golang.org/appengine v1.6.7 // indirect 112 | google.golang.org/protobuf v1.32.0 // indirect 113 | gopkg.in/inf.v0 v0.9.1 // indirect 114 | gopkg.in/ini.v1 v1.67.0 // indirect 115 | gopkg.in/resty.v1 v1.12.0 // indirect 116 | gopkg.in/yaml.v2 v2.4.0 // indirect 117 | gopkg.in/yaml.v3 v3.0.1 // indirect 118 | k8s.io/apiextensions-apiserver v0.25.4 // indirect 119 | k8s.io/component-base v0.25.4 // indirect 120 | k8s.io/klog/v2 v2.80.1 // indirect 121 | k8s.io/kube-openapi v0.0.0-20221116234839-dd070e2c4cb3 // indirect 122 | k8s.io/utils v0.0.0-20221108210102-8e77b1f39fe2 // indirect 123 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 124 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 125 | sigs.k8s.io/yaml v1.3.0 // indirect 126 | ) 127 | 128 | replace ( 129 | github.com/go-ldap/ldap/v3 v3.1.10 => github.com/go-ldap/ldap/v3 v3.4.4 130 | github.com/nats-io/nats-server/v2 v2.5.0 => github.com/nats-io/nats-server/v2 v2.9.10 131 | ) 132 | -------------------------------------------------------------------------------- /helm/cert-operator/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | name: cert-operator 3 | version: [[ .Version ]] 4 | appVersion: [[ .AppVersion ]] 5 | description: >- 6 | cert-operator creates/manages certificates for Kubernetes clusters running on 7 | Giantnetes 8 | home: https://github.com/giantswarm/cert-operator 9 | annotations: 10 | application.giantswarm.io/owners: | 11 | - provider: aws 12 | team: phoenix 13 | - provider: azure 14 | team: phoenix 15 | - provider: kvm 16 | team: rocket 17 | config.giantswarm.io/version: 1.x.x 18 | application.giantswarm.io/team: phoenix 19 | -------------------------------------------------------------------------------- /helm/cert-operator/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "name" -}} 6 | {{- .Chart.Name | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create chart name and version as used by the chart label. 11 | */}} 12 | {{- define "chart" -}} 13 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 14 | {{- end -}} 15 | 16 | {{/* 17 | Common labels 18 | */}} 19 | {{- define "labels.common" -}} 20 | app: {{ include "name" . | quote }} 21 | {{ include "labels.selector" . }} 22 | application.giantswarm.io/branch: {{ .Values.project.branch | quote }} 23 | application.giantswarm.io/commit: {{ .Values.project.commit | quote }} 24 | app.kubernetes.io/managed-by: {{ .Release.Service | quote }} 25 | app.kubernetes.io/version: "{{ .Chart.AppVersion }}{{- if eq $.Chart.Name $.Release.Name }}-unique{{ end }}" 26 | {{- $regexToFind := printf "- provider:\\s%s\n\\s*team:\\s(.+)" .Values.provider.kind }} 27 | application.giantswarm.io/team: {{ index .Chart.Annotations "application.giantswarm.io/team" | quote }} 28 | helm.sh/chart: {{ include "chart" . | quote }} 29 | {{- end -}} 30 | 31 | {{/* 32 | Selector labels 33 | */}} 34 | {{- define "labels.selector" -}} 35 | app.kubernetes.io/name: {{ include "name" . | quote }} 36 | app.kubernetes.io/instance: {{ .Release.Name | quote }} 37 | {{- end -}} 38 | -------------------------------------------------------------------------------- /helm/cert-operator/templates/_resource.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Create a name stem for resource names 4 | 5 | When pods for deployments are created they have an additional 16 character 6 | suffix appended, e.g. "-957c9d6ff-pkzgw". Given that Kubernetes allows 63 7 | characters for resource names, the stem is truncated to 47 characters to leave 8 | room for such suffix. 9 | */}} 10 | {{- define "resource.default.name" -}} 11 | {{- .Release.Name | replace "." "-" | trunc 47 | trimSuffix "-" -}} 12 | {{- end -}} 13 | 14 | {{- define "resource.psp.name" -}} 15 | {{- include "resource.default.name" . -}}-psp 16 | {{- end -}} 17 | 18 | {{- define "resource.pullSecret.name" -}} 19 | {{- include "resource.default.name" . -}}-pull-secret 20 | {{- end -}} 21 | 22 | {{- define "resource.default.namespace" -}} 23 | giantswarm 24 | {{- end -}} 25 | 26 | {{/* 27 | The deployment of cert-operator for management cluster reconciles a special certconfig 28 | CR version of 0.0.0. 29 | */}} 30 | {{- define "resource.app.unique" -}} 31 | {{- if eq $.Chart.Name $.Release.Name }}true{{ else }}false{{ end }} 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /helm/cert-operator/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "resource.default.name" . }} 5 | namespace: {{ include "resource.default.namespace" . }} 6 | data: 7 | config.yaml: | 8 | server: 9 | enable: 10 | debug: 11 | server: true 12 | listen: 13 | address: 'http://0.0.0.0:8000' 14 | service: 15 | app: 16 | unique: {{ include "resource.app.unique" . }} 17 | crd: 18 | labelSelector: '{{ .Values.crd.labelSelector }}' 19 | kubernetes: 20 | address: '' 21 | inCluster: true 22 | tls: 23 | caFile: '' 24 | crtFile: '' 25 | keyFile: '' 26 | resource: 27 | vaultCrt: 28 | expirationThreshold: '{{ .Values.resource.expirationThreshold }}' 29 | namespace: 'default' 30 | vault: 31 | config: 32 | address: '{{ .Values.vault.address }}' 33 | pki: 34 | ca: 35 | ttl: '{{ .Values.vault.ca.ttl }}' 36 | commonname: 37 | format: '%s.{{ .Values.workloadCluster.kubernetes.api.endpointBase }}' 38 | -------------------------------------------------------------------------------- /helm/cert-operator/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "resource.default.name" . }} 5 | namespace: {{ include "resource.default.namespace" . }} 6 | labels: 7 | {{- include "labels.common" . | nindent 4 }} 8 | spec: 9 | replicas: 1 10 | revisionHistoryLimit: 3 11 | strategy: 12 | type: Recreate 13 | selector: 14 | matchLabels: 15 | {{- include "labels.selector" . | nindent 6 }} 16 | template: 17 | metadata: 18 | labels: 19 | {{- include "labels.selector" . | nindent 8 }} 20 | annotations: 21 | releaseRevision: {{ .Release.Revision | quote }} 22 | spec: 23 | volumes: 24 | - name: {{ include "name" . }}-configmap 25 | configMap: 26 | name: {{ include "resource.default.name" . }} 27 | items: 28 | - key: config.yaml 29 | path: config.yaml 30 | - name: certs 31 | hostPath: 32 | path: /etc/ssl/certs/ca-certificates.crt 33 | - name: ssl-certs 34 | hostPath: 35 | path: /etc/ssl/certs/ 36 | serviceAccountName: {{ include "resource.default.name" . }} 37 | securityContext: 38 | runAsUser: {{ .Values.userID }} 39 | runAsGroup: {{ .Values.groupID }} 40 | {{- with .Values.podSecurityContext }} 41 | {{- . | toYaml | nindent 8 }} 42 | {{- end }} 43 | initContainers: 44 | - args: 45 | - --vault-address={{ .Values.vault.address }} 46 | - --vault-role=cert-operator 47 | - --vault-token-secret-name={{ include "resource.default.name" . }}-vault-token 48 | - --vault-token-secret-namespace={{ include "resource.default.namespace" . }} 49 | image: "{{ .Values.registry.domain }}/{{ .Values.k8sJwtToVaultTokenImage.name}}:{{ .Values.k8sJwtToVaultTokenImage.tag }}" 50 | imagePullPolicy: Always 51 | name: ensure-vault-token 52 | securityContext: 53 | {{- with .Values.securityContext.initContainers }} 54 | {{- . | toYaml | nindent 10 }} 55 | {{- end }} 56 | containers: 57 | - name: cert-operator 58 | image: "{{ .Values.registry.domain }}/giantswarm/cert-operator:{{ .Values.image.tag }}" 59 | volumeMounts: 60 | - name: {{ include "name" . }}-configmap 61 | mountPath: /var/run/cert-operator/configmap/ 62 | - name: certs 63 | mountPath: /etc/ssl/certs/ca-certificate.crt 64 | - name: ssl-certs 65 | mountPath: /etc/ssl/certs/ 66 | ports: 67 | - name: http 68 | containerPort: 8000 69 | args: 70 | - daemon 71 | - --config.dirs=/var/run/cert-operator/configmap/ 72 | - --config.dirs=/var/run/cert-operator/secret/ 73 | - --config.files=config 74 | - --config.files=secret 75 | - --service.vault.config.token=$(VAULT_TOKEN) 76 | env: 77 | - name: VAULT_TOKEN 78 | valueFrom: 79 | secretKeyRef: 80 | key: token 81 | name: {{ include "resource.default.name" . }}-vault-token 82 | livenessProbe: 83 | httpGet: 84 | path: /healthz 85 | port: 8000 86 | initialDelaySeconds: 15 87 | timeoutSeconds: 1 88 | readinessProbe: 89 | httpGet: 90 | path: /healthz 91 | port: 8000 92 | initialDelaySeconds: 15 93 | timeoutSeconds: 1 94 | securityContext: 95 | {{- with .Values.securityContext.default }} 96 | {{- . | toYaml | nindent 10 }} 97 | {{- end }} 98 | resources: 99 | requests: 100 | cpu: 100m 101 | memory: 20Mi 102 | limits: 103 | cpu: 250m 104 | memory: 250Mi 105 | -------------------------------------------------------------------------------- /helm/cert-operator/templates/np.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: NetworkPolicy 3 | metadata: 4 | labels: 5 | {{- include "labels.common" . | nindent 4 }} 6 | name: {{ include "resource.default.name" . }} 7 | namespace: {{ include "resource.default.namespace" . }} 8 | spec: 9 | egress: 10 | - {} 11 | ingress: 12 | - ports: 13 | - port: 8000 14 | protocol: TCP 15 | podSelector: 16 | matchLabels: 17 | {{- include "labels.selector" . | nindent 6 }} 18 | policyTypes: 19 | - Egress 20 | - Ingress 21 | -------------------------------------------------------------------------------- /helm/cert-operator/templates/psp.yaml: -------------------------------------------------------------------------------- 1 | {{- if not .Values.global.podSecurityStandards.enforced }} 2 | apiVersion: policy/v1beta1 3 | kind: PodSecurityPolicy 4 | metadata: 5 | name: {{ include "resource.psp.name" . }} 6 | annotations: 7 | seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'runtime/default' 8 | labels: 9 | {{- include "labels.common" . | nindent 4 }} 10 | spec: 11 | privileged: false 12 | fsGroup: 13 | rule: MustRunAs 14 | ranges: 15 | - min: 1 16 | max: 65535 17 | runAsUser: 18 | rule: MustRunAsNonRoot 19 | runAsGroup: 20 | rule: MustRunAs 21 | ranges: 22 | - min: 1 23 | max: 65535 24 | seLinux: 25 | rule: RunAsAny 26 | supplementalGroups: 27 | rule: RunAsAny 28 | volumes: 29 | - 'secret' 30 | - 'configMap' 31 | - 'hostPath' 32 | allowPrivilegeEscalation: false 33 | hostNetwork: false 34 | hostIPC: false 35 | hostPID: false 36 | {{- end }} 37 | -------------------------------------------------------------------------------- /helm/cert-operator/templates/pss-exceptions.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kyverno.io/v2alpha1 2 | kind: PolicyException 3 | metadata: 4 | name: {{ include "resource.default.name" . }}-exceptions 5 | namespace: {{ include "resource.default.namespace" . }} 6 | spec: 7 | exceptions: 8 | - policyName: restrict-volume-types 9 | ruleNames: 10 | - restricted-volumes 11 | - autogen-restricted-volumes 12 | - policyName: disallow-host-path 13 | ruleNames: 14 | - host-path 15 | - autogen-host-path 16 | - policyName: disallow-capabilities-strict 17 | ruleNames: 18 | - adding-capabilities-strict 19 | - autogen-adding-capabilities-strict 20 | - require-drop-all 21 | - autogen-require-drop-all 22 | - policyName: disallow-privilege-escalation 23 | ruleNames: 24 | - privilege-escalation 25 | - autogen-privilege-escalation 26 | - policyName: require-run-as-nonroot 27 | ruleNames: 28 | - run-as-non-root 29 | - autogen-run-as-non-root 30 | - policyName: restrict-seccomp-strict 31 | ruleNames: 32 | - check-seccomp-strict 33 | - autogen-check-seccomp-strict 34 | match: 35 | any: 36 | - resources: 37 | kinds: 38 | - Deployment 39 | - ReplicaSet 40 | - Pod 41 | namespaces: 42 | - {{ include "resource.default.namespace" . }} 43 | names: 44 | - {{ .Chart.Name }}* 45 | -------------------------------------------------------------------------------- /helm/cert-operator/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "resource.default.name" . }} 5 | labels: 6 | {{- include "labels.common" . | nindent 4 }} 7 | rules: 8 | - apiGroups: 9 | - apiextensions.k8s.io 10 | resources: 11 | - customresourcedefinitions 12 | verbs: 13 | - "*" 14 | - apiGroups: 15 | - extensions 16 | resources: 17 | - thirdpartyresources 18 | verbs: 19 | - "*" 20 | - apiGroups: 21 | - core.giantswarm.io 22 | resources: 23 | - certconfigs 24 | verbs: 25 | - "*" 26 | - apiGroups: 27 | - "" 28 | resources: 29 | - pods 30 | verbs: 31 | - get 32 | - list 33 | - apiGroups: 34 | - cluster.x-k8s.io 35 | resources: 36 | - clusters 37 | verbs: 38 | - get 39 | - list 40 | - apiGroups: 41 | - provider.giantswarm.io 42 | resources: 43 | - azureconfigs 44 | - kvmconfigs 45 | verbs: 46 | - get 47 | - apiGroups: 48 | - "" 49 | resources: 50 | - namespaces 51 | verbs: 52 | - get 53 | - create 54 | - apiGroups: 55 | - "" 56 | resources: 57 | - secrets 58 | verbs: 59 | - get 60 | - create 61 | - update 62 | - delete 63 | - apiGroups: 64 | - "" 65 | resources: 66 | - configmaps 67 | resourceNames: 68 | - {{ include "resource.default.name" . }} 69 | verbs: 70 | - get 71 | - nonResourceURLs: 72 | - "/" 73 | verbs: 74 | - get 75 | --- 76 | apiVersion: rbac.authorization.k8s.io/v1 77 | kind: ClusterRoleBinding 78 | metadata: 79 | name: {{ include "resource.default.name" . }} 80 | labels: 81 | {{- include "labels.common" . | nindent 4 }} 82 | subjects: 83 | - kind: ServiceAccount 84 | name: {{ include "resource.default.name" . }} 85 | namespace: {{ include "resource.default.namespace" . }} 86 | roleRef: 87 | kind: ClusterRole 88 | name: {{ include "resource.default.name" . }} 89 | apiGroup: rbac.authorization.k8s.io 90 | --- 91 | apiVersion: rbac.authorization.k8s.io/v1 92 | kind: ClusterRole 93 | metadata: 94 | name: {{ include "resource.psp.name" . }} 95 | labels: 96 | {{- include "labels.common" . | nindent 4 }} 97 | rules: 98 | - apiGroups: 99 | - extensions 100 | resources: 101 | - podsecuritypolicies 102 | verbs: 103 | - use 104 | resourceNames: 105 | - {{ include "resource.psp.name" . }} 106 | --- 107 | apiVersion: rbac.authorization.k8s.io/v1 108 | kind: ClusterRoleBinding 109 | metadata: 110 | name: {{ include "resource.psp.name" . }} 111 | labels: 112 | {{- include "labels.common" . | nindent 4 }} 113 | subjects: 114 | - kind: ServiceAccount 115 | name: {{ include "resource.default.name" . }} 116 | namespace: {{ include "resource.default.namespace" . }} 117 | roleRef: 118 | kind: ClusterRole 119 | name: {{ include "resource.psp.name" . }} 120 | apiGroup: rbac.authorization.k8s.io 121 | -------------------------------------------------------------------------------- /helm/cert-operator/templates/service-account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ include "resource.default.name" . }} 5 | namespace: {{ include "resource.default.namespace" . }} 6 | labels: 7 | {{- include "labels.common" . | nindent 4 }} 8 | -------------------------------------------------------------------------------- /helm/cert-operator/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "resource.default.name" . }} 5 | namespace: {{ include "resource.default.namespace" . }} 6 | labels: 7 | {{- include "labels.common" . | nindent 4 }} 8 | spec: 9 | ports: 10 | - name: http 11 | port: 8000 12 | protocol: TCP 13 | targetPort: http 14 | selector: 15 | {{- include "labels.selector" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /helm/cert-operator/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceMonitor.enabled }} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | name: {{ include "resource.default.name" . }} 6 | namespace: {{ include "resource.default.namespace" . }} 7 | labels: 8 | {{- include "labels.common" . | nindent 4 }} 9 | spec: 10 | endpoints: 11 | - interval: {{ .Values.serviceMonitor.interval }} 12 | path: /metrics 13 | port: http 14 | scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }} 15 | selector: 16 | matchLabels: 17 | {{- include "labels.selector" . | nindent 6 }} 18 | {{- end }} 19 | -------------------------------------------------------------------------------- /helm/cert-operator/values.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema#", 3 | "type": "object", 4 | "properties": { 5 | "crd": { 6 | "type": "object", 7 | "properties": { 8 | "labelSelector": { 9 | "type": "string" 10 | } 11 | } 12 | }, 13 | "groupID": { 14 | "type": "integer" 15 | }, 16 | "image": { 17 | "type": "object", 18 | "properties": { 19 | "tag": { 20 | "type": "string" 21 | } 22 | } 23 | }, 24 | "k8sJwtToVaultTokenImage": { 25 | "type": "object", 26 | "properties": { 27 | "name": { 28 | "type": "string" 29 | }, 30 | "tag": { 31 | "type": "string" 32 | } 33 | } 34 | }, 35 | "podSecurityContext": { 36 | "type": "object", 37 | "properties": { 38 | "runAsNonRoot": { 39 | "type": "boolean" 40 | }, 41 | "seccompProfile": { 42 | "type": "object", 43 | "properties": { 44 | "type": { 45 | "type": "string" 46 | } 47 | } 48 | } 49 | } 50 | }, 51 | "project": { 52 | "type": "object", 53 | "properties": { 54 | "branch": { 55 | "type": "string" 56 | }, 57 | "commit": { 58 | "type": "string" 59 | } 60 | } 61 | }, 62 | "provider": { 63 | "type": "object", 64 | "properties": { 65 | "kind": { 66 | "type": "string" 67 | } 68 | } 69 | }, 70 | "registry": { 71 | "type": "object", 72 | "properties": { 73 | "domain": { 74 | "type": "string" 75 | } 76 | } 77 | }, 78 | "resource": { 79 | "type": "object", 80 | "properties": { 81 | "expirationThreshold": { 82 | "type": "string" 83 | } 84 | } 85 | }, 86 | "securityContext": { 87 | "type": "object", 88 | "properties": { 89 | "default": { 90 | "type": "object", 91 | "properties": { 92 | "allowPrivilegeEscalation": { 93 | "type": "boolean" 94 | }, 95 | "capabilities": { 96 | "type": "object", 97 | "properties": { 98 | "drop": { 99 | "type": "array", 100 | "items": { 101 | "type": "string" 102 | } 103 | } 104 | } 105 | }, 106 | "seccompProfile": { 107 | "type": "object", 108 | "properties": { 109 | "type": { 110 | "type": "string" 111 | } 112 | } 113 | } 114 | } 115 | }, 116 | "initContainers": { 117 | "type": "object", 118 | "properties": { 119 | "allowPrivilegeEscalation": { 120 | "type": "boolean" 121 | }, 122 | "capabilities": { 123 | "type": "object", 124 | "properties": { 125 | "drop": { 126 | "type": "array", 127 | "items": { 128 | "type": "string" 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | }, 137 | "serviceMonitor": { 138 | "type": "object", 139 | "properties": { 140 | "enabled": { 141 | "type": "boolean" 142 | }, 143 | "interval": { 144 | "type": "string" 145 | }, 146 | "scrapeTimeout": { 147 | "type": "string" 148 | } 149 | } 150 | }, 151 | "userID": { 152 | "type": "integer" 153 | }, 154 | "vault": { 155 | "type": "object", 156 | "properties": { 157 | "address": { 158 | "type": "string" 159 | }, 160 | "ca": { 161 | "type": "object", 162 | "properties": { 163 | "ttl": { 164 | "type": "string" 165 | } 166 | } 167 | } 168 | } 169 | }, 170 | "workloadCluster": { 171 | "type": "object", 172 | "properties": { 173 | "kubernetes": { 174 | "type": "object", 175 | "properties": { 176 | "api": { 177 | "type": "object", 178 | "properties": { 179 | "endpointBase": { 180 | "type": "string" 181 | } 182 | } 183 | } 184 | } 185 | } 186 | } 187 | }, 188 | "global": { 189 | "type": "object", 190 | "properties": { 191 | "podSecurityStandards": { 192 | "type": "object", 193 | "properties": { 194 | "enforced": { 195 | "type": "boolean" 196 | } 197 | } 198 | } 199 | } 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /helm/cert-operator/values.yaml: -------------------------------------------------------------------------------- 1 | project: 2 | branch: "[[ .Branch ]]" 3 | commit: "[[ .SHA ]]" 4 | image: 5 | tag: "[[ .Version ]]" 6 | userID: 1000 7 | groupID: 1000 8 | 9 | crd: 10 | labelSelector: "" 11 | 12 | k8sJwtToVaultTokenImage: 13 | name: giantswarm/k8s-jwt-to-vault-token 14 | tag: 0.1.0 15 | 16 | registry: 17 | domain: gsoci.azurecr.io 18 | 19 | resource: 20 | expirationThreshold: "2160h" 21 | 22 | vault: 23 | address: "" 24 | ca: 25 | ttl: "87600h" 26 | 27 | workloadCluster: 28 | kubernetes: 29 | api: 30 | endpointBase: "" 31 | 32 | provider: 33 | kind: "aws" 34 | 35 | # Add seccomp to pod security context 36 | podSecurityContext: 37 | runAsNonRoot: true 38 | seccompProfile: 39 | type: RuntimeDefault 40 | 41 | # Add seccomp to container security context 42 | securityContext: 43 | default: 44 | allowPrivilegeEscalation: false 45 | seccompProfile: 46 | type: RuntimeDefault 47 | capabilities: 48 | drop: 49 | - ALL 50 | initContainers: 51 | allowPrivilegeEscalation: false 52 | capabilities: 53 | drop: 54 | - ALL 55 | 56 | serviceMonitor: 57 | enabled: true 58 | # -- (duration) Prometheus scrape interval. 59 | interval: "60s" 60 | # -- (duration) Prometheus scrape timeout. 61 | scrapeTimeout: "45s" 62 | 63 | global: 64 | podSecurityStandards: 65 | enforced: false 66 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/giantswarm/microkit/command" 7 | microserver "github.com/giantswarm/microkit/server" 8 | "github.com/giantswarm/micrologger" 9 | "github.com/spf13/viper" 10 | 11 | "github.com/giantswarm/cert-operator/v3/flag" 12 | "github.com/giantswarm/cert-operator/v3/pkg/project" 13 | "github.com/giantswarm/cert-operator/v3/server" 14 | "github.com/giantswarm/cert-operator/v3/service" 15 | ) 16 | 17 | var ( 18 | f *flag.Flag = flag.New() 19 | ) 20 | 21 | func main() { 22 | var err error 23 | 24 | // Create a new logger which is used by all packages. 25 | var newLogger micrologger.Logger 26 | { 27 | newLogger, err = micrologger.New(micrologger.Config{}) 28 | if err != nil { 29 | panic(fmt.Sprintf("%#v\n", err)) 30 | } 31 | } 32 | 33 | // We define a server factory to create the custom server once all command 34 | // line flags are parsed and all microservice configuration is storted out. 35 | newServerFactory := func(v *viper.Viper) microserver.Server { 36 | // Create a new custom service which implements business logic. 37 | var newService *service.Service 38 | { 39 | serviceConfig := service.Config{ 40 | Flag: f, 41 | Logger: newLogger, 42 | Viper: v, 43 | 44 | Description: project.Description(), 45 | GitCommit: project.GitSHA(), 46 | ProjectName: project.Name(), 47 | Source: project.Source(), 48 | Version: project.Version(), 49 | } 50 | 51 | newService, err = service.New(serviceConfig) 52 | if err != nil { 53 | panic(fmt.Sprintf("%#v\n", err)) 54 | } 55 | go newService.Boot() 56 | } 57 | 58 | // Create a new custom server which bundles our endpoints. 59 | var newServer microserver.Server 60 | { 61 | c := server.Config{ 62 | Logger: newLogger, 63 | Service: newService, 64 | Viper: v, 65 | 66 | ProjectName: project.Name(), 67 | } 68 | 69 | newServer, err = server.New(c) 70 | if err != nil { 71 | panic(fmt.Sprintf("%#v\n", err)) 72 | } 73 | } 74 | 75 | return newServer 76 | } 77 | 78 | // Create a new microkit command which manages our custom microservice. 79 | var newCommand command.Command 80 | { 81 | c := command.Config{ 82 | Logger: newLogger, 83 | ServerFactory: newServerFactory, 84 | 85 | Description: project.Description(), 86 | GitCommit: project.GitSHA(), 87 | Name: project.Name(), 88 | Source: project.Source(), 89 | Version: project.Version(), 90 | } 91 | 92 | newCommand, err = command.New(c) 93 | if err != nil { 94 | panic(fmt.Sprintf("%#v\n", err)) 95 | } 96 | } 97 | 98 | daemonCommand := newCommand.DaemonCommand().CobraCommand() 99 | 100 | daemonCommand.PersistentFlags().String(f.Service.CRD.LabelSelector, "", "Label selector for CRD informer ListOptions.") 101 | 102 | daemonCommand.PersistentFlags().String(f.Service.Kubernetes.Address, "http://127.0.0.1:6443", "Address used to connect to Kubernetes. When empty in-cluster config is created.") 103 | daemonCommand.PersistentFlags().Bool(f.Service.Kubernetes.InCluster, false, "Whether to use the in-cluster config to authenticate with Kubernetes.") 104 | daemonCommand.PersistentFlags().String(f.Service.Kubernetes.KubeConfig, "", "KubeConfig used to connect to Kubernetes. When empty other settings are used.") 105 | daemonCommand.PersistentFlags().String(f.Service.Kubernetes.TLS.CAFile, "", "Certificate authority file path to use to authenticate with Kubernetes.") 106 | daemonCommand.PersistentFlags().String(f.Service.Kubernetes.TLS.CrtFile, "", "Certificate file path to use to authenticate with Kubernetes.") 107 | daemonCommand.PersistentFlags().String(f.Service.Kubernetes.TLS.KeyFile, "", "Key file path to use to authenticate with Kubernetes.") 108 | 109 | daemonCommand.PersistentFlags().Duration(f.Service.Resource.VaultCrt.ExpirationThreshold, 0, "Amount of time to renew certificates before their expiration date.") 110 | daemonCommand.PersistentFlags().String(f.Service.Resource.VaultCrt.Namespace, "", "Namespace used to manage Kubernetes secrets in.") 111 | 112 | daemonCommand.PersistentFlags().Bool(f.Service.App.Unique, false, "Whether the operator is deployed as a unique app.") 113 | daemonCommand.PersistentFlags().String(f.Service.Vault.Config.Address, "", "Address used to connect to Vault.") 114 | daemonCommand.PersistentFlags().String(f.Service.Vault.Config.Token, "", "Token used to authenticate against Vault.") 115 | daemonCommand.PersistentFlags().String(f.Service.Vault.Config.PKI.CA.TTL, "", "TTL used to generate a new Cluster CA.") 116 | daemonCommand.PersistentFlags().String(f.Service.Vault.Config.PKI.CommonName.Format, "", "Common name used to generate a new Cluster CA.") 117 | 118 | if err := newCommand.CobraCommand().Execute(); err != nil { 119 | panic(fmt.Sprintf("%#v\n", err)) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /pkg/label/label.go: -------------------------------------------------------------------------------- 1 | package label 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/labels" 5 | 6 | "github.com/giantswarm/cert-operator/v3/pkg/project" 7 | ) 8 | 9 | const ( 10 | Cluster = "giantswarm.io/cluster" 11 | OperatorVersion = "cert-operator.giantswarm.io/version" 12 | ) 13 | 14 | func AppVersionSelector() labels.Selector { 15 | return labels.SelectorFromSet(map[string]string{ 16 | OperatorVersion: project.Version(), 17 | }) 18 | } 19 | 20 | // KubeconfigSelector selects all certconfigs that use the special version `0.0.0`. 21 | func KubeconfigSelector() labels.Selector { 22 | return labels.SelectorFromSet(map[string]string{ 23 | OperatorVersion: project.ManagementClusterAppVersion(), 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/project/project.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | var ( 4 | description string = "The cert-operator handles certificates for Kubernetes clusters running on Giantnetes." 5 | gitSHA = "n/a" 6 | name string = "cert-operator" 7 | source string = "https://github.com/giantswarm/cert-operator" 8 | version = "3.4.1-dev" 9 | ) 10 | 11 | func Description() string { 12 | return description 13 | } 14 | 15 | func GitSHA() string { 16 | return gitSHA 17 | } 18 | 19 | func Name() string { 20 | return name 21 | } 22 | 23 | func Source() string { 24 | return source 25 | } 26 | 27 | func Version() string { 28 | return version 29 | } 30 | 31 | // ManagementClusterAppVersion is always 0.0.0 for management cluster app CRs. These CRs 32 | // are processed by app-operator-unique which always runs the latest version. 33 | func ManagementClusterAppVersion() string { 34 | return "0.0.0" 35 | } 36 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | // Base config - https://github.com/giantswarm/renovate-presets/blob/main/default.json5 4 | "github>giantswarm/renovate-presets:default.json5", 5 | // Go specific config - https://github.com/giantswarm/renovate-presets/blob/main/lang-go.json5 6 | "github>giantswarm/renovate-presets:lang-go.json5", 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /server/endpoint/endpoint.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "github.com/giantswarm/microendpoint/endpoint/healthz" 5 | "github.com/giantswarm/microendpoint/endpoint/version" 6 | "github.com/giantswarm/microerror" 7 | "github.com/giantswarm/micrologger" 8 | 9 | "github.com/giantswarm/cert-operator/v3/service" 10 | ) 11 | 12 | // Config represents the configuration used to create a endpoint. 13 | type Config struct { 14 | Logger micrologger.Logger 15 | Service *service.Service 16 | } 17 | 18 | // Endpoint is the endpoint collection. 19 | type Endpoint struct { 20 | Healthz *healthz.Endpoint 21 | Version *version.Endpoint 22 | } 23 | 24 | // New creates a new configured endpoint. 25 | func New(config Config) (*Endpoint, error) { 26 | var err error 27 | 28 | var healthzEndpoint *healthz.Endpoint 29 | { 30 | c := healthz.Config{ 31 | Logger: config.Logger, 32 | } 33 | 34 | healthzEndpoint, err = healthz.New(c) 35 | if err != nil { 36 | return nil, microerror.Mask(err) 37 | } 38 | } 39 | 40 | var versionEndpoint *version.Endpoint 41 | { 42 | c := version.Config{ 43 | Logger: config.Logger, 44 | Service: config.Service.Version, 45 | } 46 | 47 | versionEndpoint, err = version.New(c) 48 | if err != nil { 49 | return nil, microerror.Mask(err) 50 | } 51 | } 52 | 53 | newEndpoint := &Endpoint{ 54 | Healthz: healthzEndpoint, 55 | Version: versionEndpoint, 56 | } 57 | 58 | return newEndpoint, nil 59 | } 60 | -------------------------------------------------------------------------------- /server/endpoint/error.go: -------------------------------------------------------------------------------- 1 | package endpoint 2 | 3 | import ( 4 | "github.com/giantswarm/microerror" 5 | ) 6 | 7 | var invalidConfigError = µerror.Error{ 8 | Kind: "invalidConfigError", 9 | } 10 | 11 | // IsInvalidConfig asserts invalidConfigError. 12 | func IsInvalidConfig(err error) bool { 13 | return microerror.Cause(err) == invalidConfigError 14 | } 15 | -------------------------------------------------------------------------------- /server/error.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/giantswarm/microerror" 5 | ) 6 | 7 | var invalidConfigError = µerror.Error{ 8 | Kind: "invalidConfigError", 9 | } 10 | 11 | // IsInvalidConfig asserts invalidConfigError. 12 | func IsInvalidConfig(err error) bool { 13 | return microerror.Cause(err) == invalidConfigError 14 | } 15 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | // Package server provides a server implementation to connect network transport 2 | // protocols and service business logic by defining server endpoints. 3 | package server 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | "sync" 9 | 10 | "github.com/giantswarm/microerror" 11 | microserver "github.com/giantswarm/microkit/server" 12 | "github.com/giantswarm/micrologger" 13 | "github.com/spf13/viper" 14 | 15 | "github.com/giantswarm/cert-operator/v3/server/endpoint" 16 | "github.com/giantswarm/cert-operator/v3/service" 17 | ) 18 | 19 | type Config struct { 20 | Logger micrologger.Logger 21 | Service *service.Service 22 | Viper *viper.Viper 23 | 24 | ProjectName string 25 | } 26 | 27 | type Server struct { 28 | // Dependencies. 29 | logger micrologger.Logger 30 | 31 | // Internals. 32 | bootOnce sync.Once 33 | config microserver.Config 34 | shutdownOnce sync.Once 35 | } 36 | 37 | // New creates a new configured server object. 38 | func New(config Config) (*Server, error) { 39 | if config.Logger == nil { 40 | return nil, microerror.Maskf(invalidConfigError, "%T.Logger must not be empty", config) 41 | } 42 | if config.Service == nil { 43 | return nil, microerror.Maskf(invalidConfigError, "%T.Service must not be empty", config) 44 | } 45 | if config.Viper == nil { 46 | return nil, microerror.Maskf(invalidConfigError, "%T.Viper must not be empty", config) 47 | } 48 | 49 | if config.ProjectName == "" { 50 | return nil, microerror.Maskf(invalidConfigError, "%T.ProjectName must not be empty", config) 51 | } 52 | 53 | var err error 54 | 55 | var endpointCollection *endpoint.Endpoint 56 | { 57 | c := endpoint.Config{ 58 | Logger: config.Logger, 59 | Service: config.Service, 60 | } 61 | 62 | endpointCollection, err = endpoint.New(c) 63 | if err != nil { 64 | return nil, microerror.Mask(err) 65 | } 66 | } 67 | 68 | s := &Server{ 69 | logger: config.Logger, 70 | 71 | bootOnce: sync.Once{}, 72 | config: microserver.Config{ 73 | Logger: config.Logger, 74 | ServiceName: config.ProjectName, 75 | Viper: config.Viper, 76 | 77 | Endpoints: []microserver.Endpoint{ 78 | endpointCollection.Healthz, 79 | endpointCollection.Version, 80 | }, 81 | ErrorEncoder: errorEncoder, 82 | }, 83 | shutdownOnce: sync.Once{}, 84 | } 85 | 86 | return s, nil 87 | } 88 | 89 | func (s *Server) Boot() { 90 | s.bootOnce.Do(func() { 91 | // Here goes your custom boot logic for your server/endpoint/middleware, if 92 | // any. 93 | }) 94 | } 95 | 96 | func (s *Server) Config() microserver.Config { 97 | return s.config 98 | } 99 | 100 | func (s *Server) Shutdown() { 101 | s.shutdownOnce.Do(func() { 102 | // Here goes your custom shutdown logic for your server/endpoint/middleware, 103 | // if any. 104 | }) 105 | } 106 | 107 | func errorEncoder(ctx context.Context, err error, w http.ResponseWriter) { 108 | rErr := err.(microserver.ResponseError) 109 | uErr := rErr.Underlying() 110 | 111 | rErr.SetCode(microserver.CodeInternalError) 112 | rErr.SetMessage(uErr.Error()) 113 | w.WriteHeader(http.StatusInternalServerError) 114 | } 115 | -------------------------------------------------------------------------------- /service/collector/error.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/giantswarm/microerror" 7 | ) 8 | 9 | // executionFailedError is an error type for situations where Resource execution 10 | // cannot continue and must always fall back to operatorkit. 11 | // 12 | // This error should never be matched against and therefore there is no matcher 13 | // implement. For further information see: 14 | // 15 | // https://github.com/giantswarm/fmt/blob/master/go/errors.md#matching-errors 16 | var executionFailedError = µerror.Error{ 17 | Kind: "executionFailedError", 18 | } 19 | 20 | var invalidConfigError = µerror.Error{ 21 | Kind: "invalidConfigError", 22 | } 23 | 24 | // IsInvalidConfig asserts invalidConfigError. 25 | func IsInvalidConfig(err error) bool { 26 | return microerror.Cause(err) == invalidConfigError 27 | } 28 | 29 | var vaultAccessError = µerror.Error{ 30 | Kind: "vaultAccessError", 31 | } 32 | 33 | // IsVaultAccess asserts vaultAccessError. The matcher also asserts errors 34 | // caused by situations in which Vault is updated strategically and thus 35 | // temporarily replies with HTTP responses. In such cases we intend to cancel 36 | // collection and wait until Vault is fully operational again. 37 | // 38 | // Get https://vault.g8s.foo.bar:8200/v1/sys/mounts: http: server gave HTTP response to HTTPS client 39 | func IsVaultAccess(err error) bool { 40 | if err == nil { 41 | return false 42 | } 43 | 44 | c := microerror.Cause(err) 45 | 46 | if strings.Contains(c.Error(), "server gave HTTP response to HTTPS client") { 47 | return true 48 | } 49 | 50 | if c == vaultAccessError { 51 | return true 52 | } 53 | 54 | return false 55 | } 56 | -------------------------------------------------------------------------------- /service/collector/set.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "github.com/giantswarm/exporterkit/collector" 5 | "github.com/giantswarm/microerror" 6 | "github.com/giantswarm/micrologger" 7 | vault "github.com/hashicorp/vault/api" 8 | ) 9 | 10 | type SetConfig struct { 11 | Logger micrologger.Logger 12 | VaultClient *vault.Client 13 | } 14 | 15 | // Set is basically only a wrapper for the operator's collector implementations. 16 | // It eases the iniitialization and prevents some weird import mess so we do not 17 | // have to alias packages. 18 | type Set struct { 19 | *collector.Set 20 | } 21 | 22 | func NewSet(config SetConfig) (*Set, error) { 23 | var err error 24 | 25 | var vaultCollector *Vault 26 | { 27 | c := VaultConfig(config) 28 | vaultCollector, err = NewVault(c) 29 | if err != nil { 30 | return nil, microerror.Mask(err) 31 | } 32 | } 33 | 34 | var collectorSet *collector.Set 35 | { 36 | c := collector.SetConfig{ 37 | Collectors: []collector.Interface{ 38 | vaultCollector, 39 | }, 40 | Logger: config.Logger, 41 | } 42 | 43 | collectorSet, err = collector.NewSet(c) 44 | if err != nil { 45 | return nil, microerror.Mask(err) 46 | } 47 | } 48 | 49 | s := &Set{ 50 | Set: collectorSet, 51 | } 52 | 53 | return s, nil 54 | } 55 | -------------------------------------------------------------------------------- /service/collector/vault.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | "github.com/giantswarm/microerror" 9 | "github.com/giantswarm/micrologger" 10 | vault "github.com/hashicorp/vault/api" 11 | "github.com/prometheus/client_golang/prometheus" 12 | ) 13 | 14 | const ( 15 | // ExpireTimeKey is the data key provided by the secret when looking up the 16 | // used Vault token. This key is specific to Vault as they define it. 17 | ExpireTimeKey = "expire_time" 18 | // ExpireTimeLayout is the layout used for time parsing when inspecting the 19 | // expiration date of the used Vault token. This layout is specific to Vault 20 | // as they define it. 21 | ExpireTimeLayout = "2006-01-02T15:04:05" 22 | ) 23 | 24 | var ( 25 | tokenExpireTimeDesc = prometheus.NewDesc( 26 | prometheus.BuildFQName("cert_operator", "vault", "token_expire_time_seconds"), 27 | "A metric of the expire time of Vault tokens as unix seconds.", 28 | nil, 29 | nil, 30 | ) 31 | ) 32 | 33 | type VaultConfig struct { 34 | Logger micrologger.Logger 35 | VaultClient *vault.Client 36 | } 37 | 38 | type Vault struct { 39 | logger micrologger.Logger 40 | vaultClient *vault.Client 41 | } 42 | 43 | func NewVault(config VaultConfig) (*Vault, error) { 44 | if config.Logger == nil { 45 | return nil, microerror.Maskf(invalidConfigError, "%T.Logger must not be empty", config) 46 | } 47 | if config.VaultClient == nil { 48 | return nil, microerror.Maskf(invalidConfigError, "%T.VaultClient must not be empty", config) 49 | } 50 | 51 | v := &Vault{ 52 | logger: config.Logger, 53 | vaultClient: config.VaultClient, 54 | } 55 | 56 | return v, nil 57 | } 58 | 59 | func (v *Vault) Collect(ch chan<- prometheus.Metric) error { 60 | ctx := context.Background() 61 | 62 | secret, err := v.vaultClient.Auth().Token().LookupSelf() 63 | if IsVaultAccess(err) { 64 | v.logger.LogCtx(ctx, "level", "debug", "message", "vault not reachable") 65 | v.logger.LogCtx(ctx, "level", "debug", "message", "vault upgrade in progress") 66 | v.logger.LogCtx(ctx, "level", "debug", "message", "canceling collection") 67 | return nil 68 | 69 | } else if err != nil { 70 | return microerror.Mask(err) 71 | } 72 | 73 | expiration, err := expirationFromSecret(secret) 74 | if err != nil { 75 | // Handle corner cases, when token already expired or there are some other Vault issues. 76 | // If unable to get real expiration time, set it to 0 (equal to 1970-01-01T00:00:00). 77 | ch <- prometheus.MustNewConstMetric( 78 | tokenExpireTimeDesc, 79 | prometheus.GaugeValue, 80 | 0, 81 | ) 82 | return microerror.Mask(err) 83 | } 84 | 85 | ch <- prometheus.MustNewConstMetric( 86 | tokenExpireTimeDesc, 87 | prometheus.GaugeValue, 88 | float64(expiration.Unix()), 89 | ) 90 | 91 | return nil 92 | } 93 | 94 | func (v *Vault) Describe(ch chan<- *prometheus.Desc) error { 95 | ch <- tokenExpireTimeDesc 96 | return nil 97 | } 98 | 99 | func expirationFromSecret(secret *vault.Secret) (time.Time, error) { 100 | value, ok := secret.Data[ExpireTimeKey] 101 | if !ok { 102 | return time.Time{}, microerror.Maskf(executionFailedError, "value of %q must exist in order to collect metrics for the Vault token expiration", ExpireTimeKey) 103 | } 104 | 105 | if value == nil { 106 | return time.Time{}, microerror.Maskf(executionFailedError, "Vault token does not expire, skipping metric update") 107 | } 108 | 109 | e, ok := value.(string) 110 | if !ok { 111 | return time.Time{}, microerror.Maskf(executionFailedError, "%#q must be string in order to collect metrics for the Vault token expiration", value) 112 | } 113 | 114 | split := strings.Split(e, ".") 115 | if len(split) == 0 { 116 | return time.Time{}, microerror.Maskf(executionFailedError, "%#q must have at least one item in order to collect metrics for the Vault token expiration", e) 117 | } 118 | 119 | expiration, err := time.Parse(ExpireTimeLayout, split[0]) 120 | if err != nil { 121 | return time.Time{}, microerror.Mask(err) 122 | } 123 | 124 | return expiration, nil 125 | } 126 | -------------------------------------------------------------------------------- /service/controller/cert.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | corev1alpha1 "github.com/giantswarm/apiextensions/v6/pkg/apis/core/v1alpha1" 9 | providerv1alpha1 "github.com/giantswarm/apiextensions/v6/pkg/apis/provider/v1alpha1" 10 | "github.com/giantswarm/k8sclient/v7/pkg/k8sclient" 11 | "github.com/giantswarm/microerror" 12 | "github.com/giantswarm/micrologger" 13 | "github.com/giantswarm/operatorkit/v7/pkg/controller" 14 | "github.com/giantswarm/operatorkit/v7/pkg/resource" 15 | "github.com/giantswarm/vaultcrt" 16 | "github.com/giantswarm/vaultpki" 17 | "github.com/giantswarm/vaultpki/key" 18 | "github.com/giantswarm/vaultrole" 19 | vaultapi "github.com/hashicorp/vault/api" 20 | corev1 "k8s.io/api/core/v1" 21 | "k8s.io/apimachinery/pkg/api/errors" 22 | "k8s.io/apimachinery/pkg/labels" 23 | "k8s.io/apimachinery/pkg/types" 24 | capi "sigs.k8s.io/cluster-api/api/v1beta1" 25 | "sigs.k8s.io/controller-runtime/pkg/client" 26 | 27 | "github.com/giantswarm/cert-operator/v3/pkg/label" 28 | ) 29 | 30 | type CertConfig struct { 31 | K8sClient k8sclient.Interface 32 | Logger micrologger.Logger 33 | VaultClient *vaultapi.Client 34 | 35 | UniqueApp bool 36 | CATTL string 37 | CRDLabelSelector string 38 | CommonNameFormat string 39 | ExpirationThreshold time.Duration 40 | Namespace string 41 | ProjectName string 42 | } 43 | 44 | type Cert struct { 45 | *controller.Controller 46 | } 47 | 48 | func NewCert(config CertConfig) (*Cert, error) { 49 | if config.K8sClient == nil { 50 | return nil, microerror.Maskf(invalidConfigError, "config.K8sClient must not be empty") 51 | } 52 | 53 | var err error 54 | 55 | var vaultCrt vaultcrt.Interface 56 | { 57 | c := vaultcrt.DefaultConfig() 58 | 59 | c.Logger = config.Logger 60 | c.VaultClient = config.VaultClient 61 | 62 | vaultCrt, err = vaultcrt.New(c) 63 | if err != nil { 64 | return nil, microerror.Mask(err) 65 | } 66 | } 67 | 68 | var vaultPKI vaultpki.Interface 69 | { 70 | c := vaultpki.Config{ 71 | Logger: config.Logger, 72 | VaultClient: config.VaultClient, 73 | 74 | CATTL: config.CATTL, 75 | CommonNameFormat: config.CommonNameFormat, 76 | } 77 | 78 | vaultPKI, err = vaultpki.New(c) 79 | if err != nil { 80 | return nil, microerror.Mask(err) 81 | } 82 | } 83 | 84 | var vaultRole vaultrole.Interface 85 | { 86 | c := vaultrole.DefaultConfig() 87 | 88 | c.Logger = config.Logger 89 | c.VaultClient = config.VaultClient 90 | 91 | c.CommonNameFormat = config.CommonNameFormat 92 | 93 | vaultRole, err = vaultrole.New(c) 94 | if err != nil { 95 | return nil, microerror.Mask(err) 96 | } 97 | } 98 | 99 | var resources []resource.Interface 100 | { 101 | c := ResourceSetConfig{ 102 | CtrlClient: config.K8sClient.CtrlClient(), 103 | K8sClient: config.K8sClient.K8sClient(), 104 | Logger: config.Logger, 105 | VaultClient: config.VaultClient, 106 | VaultCrt: vaultCrt, 107 | VaultPKI: vaultPKI, 108 | VaultRole: vaultRole, 109 | 110 | ExpirationThreshold: config.ExpirationThreshold, 111 | Namespace: config.Namespace, 112 | ProjectName: config.ProjectName, 113 | } 114 | 115 | resources, err = NewResourceSet(c) 116 | if err != nil { 117 | return nil, microerror.Mask(err) 118 | } 119 | } 120 | 121 | var selector labels.Selector 122 | { 123 | if config.UniqueApp { 124 | selector = label.KubeconfigSelector() 125 | } else { 126 | selector = label.AppVersionSelector() 127 | } 128 | 129 | config.Logger.Debugf(context.Background(), "Watching CertConfigs with selector %v", selector) 130 | } 131 | 132 | var operatorkitController *controller.Controller 133 | { 134 | c := controller.Config{ 135 | K8sClient: config.K8sClient, 136 | Logger: config.Logger, 137 | Name: config.ProjectName, 138 | Resources: resources, 139 | Selector: selector, 140 | NewRuntimeObjectFunc: func() client.Object { 141 | return new(corev1alpha1.CertConfig) 142 | }, 143 | } 144 | 145 | operatorkitController, err = controller.New(c) 146 | if err != nil { 147 | return nil, microerror.Mask(err) 148 | } 149 | } 150 | 151 | c := &Cert{ 152 | Controller: operatorkitController, 153 | } 154 | 155 | err = cleanupPKIBackends(config.Logger, config.K8sClient, vaultPKI) 156 | if err != nil { 157 | // We don't want a cleanup error to prevent the controller from starting. 158 | config.Logger.Log("level", "error", "message", "failed to clean up PKI backends", "stack", fmt.Sprintf("%#v", err)) 159 | } 160 | 161 | return c, nil 162 | } 163 | 164 | func cleanupPKIBackends(logger micrologger.Logger, k8sClient k8sclient.Interface, vaultPKI vaultpki.Interface) error { 165 | mounts, err := vaultPKI.ListBackends() 166 | if err != nil { 167 | return microerror.Mask(err) 168 | } 169 | 170 | logger.Log("level", "debug", "message", "cleaning up PKI backends") 171 | 172 | var latestError *error 173 | 174 | for k := range mounts { 175 | id := key.ClusterIDFromMountPath(k) 176 | 177 | exists, err := tenantClusterExists(k8sClient, id) 178 | if err != nil { 179 | return microerror.Mask(err) 180 | } 181 | 182 | if !exists { 183 | logger.Log("level", "debug", "message", fmt.Sprintf("deleting PKI backend for Tenant Cluster %#q", id)) 184 | 185 | { 186 | err := k8sClient.CtrlClient().DeleteAllOf( 187 | context.Background(), 188 | &corev1alpha1.CertConfig{}, 189 | client.MatchingLabels{label.Cluster: id}, 190 | ) 191 | if errors.IsNotFound(err) { 192 | // fall through 193 | } else if err != nil { 194 | latestError = &err 195 | logger.Log("level", "error", "message", fmt.Sprintf("error deleting certconfigs for Tenant Cluster %#q", id)) 196 | continue 197 | } 198 | } 199 | 200 | { 201 | err := vaultPKI.DeleteBackend(id) 202 | if err != nil { 203 | latestError = &err 204 | logger.Log("level", "error", "message", fmt.Sprintf("error deleting PKI backend for Tenant Cluster %#q", id)) 205 | continue 206 | } 207 | } 208 | 209 | logger.Log("level", "debug", "message", fmt.Sprintf("deleted PKI backend for Tenant Cluster %#q", id)) 210 | } 211 | } 212 | 213 | if latestError != nil { 214 | return microerror.Mask(*latestError) 215 | } 216 | 217 | logger.Log("level", "debug", "message", "cleaned up PKI backends") 218 | 219 | return nil 220 | } 221 | 222 | func tenantClusterExists(k8sClient k8sclient.Interface, id string) (bool, error) { 223 | var err error 224 | 225 | // We need to check for Node Pools clusters. These adhere to CAPI and do not 226 | // have any AWSConfig CR anymore. 227 | { 228 | crs := &capi.ClusterList{} 229 | 230 | var labelSelector client.MatchingLabels 231 | { 232 | labelSelector = make(map[string]string) 233 | labelSelector[label.Cluster] = id 234 | } 235 | 236 | err := k8sClient.CtrlClient().List(context.Background(), crs, labelSelector) 237 | if errors.IsNotFound(err) { 238 | // fall through 239 | } else if IsNoKind(err) { 240 | // fall through 241 | } else if err != nil { 242 | return false, microerror.Mask(err) 243 | } else if len(crs.Items) < 1 { 244 | // fall through 245 | } else { 246 | return true, nil 247 | } 248 | } 249 | 250 | // We need to check for the legacy KVMConfig CRs on KVM environments. 251 | { 252 | err = k8sClient.CtrlClient().Get(context.Background(), types.NamespacedName{Name: id, Namespace: corev1.NamespaceDefault}, &providerv1alpha1.KVMConfig{}) 253 | if errors.IsNotFound(err) { 254 | // fall through 255 | } else if IsNoKind(err) { 256 | // fall through 257 | } else if err != nil { 258 | return false, microerror.Mask(err) 259 | } else { 260 | return true, nil 261 | } 262 | } 263 | 264 | return false, nil 265 | } 266 | -------------------------------------------------------------------------------- /service/controller/error.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/giantswarm/microerror" 5 | "k8s.io/apimachinery/pkg/api/meta" 6 | ) 7 | 8 | var invalidConfigError = µerror.Error{ 9 | Kind: "invalidConfigError", 10 | } 11 | 12 | // IsInvalidConfig asserts invalidConfigError. 13 | func IsInvalidConfig(err error) bool { 14 | return microerror.Cause(err) == invalidConfigError 15 | } 16 | 17 | var noKindError = µerror.Error{ 18 | Kind: "noKindError", 19 | } 20 | 21 | // IsNoKind asserts noKindError. 22 | func IsNoKind(err error) bool { 23 | c := microerror.Cause(err) 24 | 25 | _, ok := c.(*meta.NoKindMatchError) 26 | if ok { 27 | return true 28 | } 29 | 30 | if c == noKindError { 31 | return true 32 | } 33 | 34 | return false 35 | } 36 | -------------------------------------------------------------------------------- /service/controller/key/error.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import "github.com/giantswarm/microerror" 4 | 5 | var wrongTypeError = µerror.Error{ 6 | Kind: "wrongTypeError", 7 | } 8 | 9 | // IsWrongTypeError asserts wrongTypeError. 10 | func IsWrongTypeError(err error) bool { 11 | return microerror.Cause(err) == wrongTypeError 12 | } 13 | -------------------------------------------------------------------------------- /service/controller/key/key.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "crypto/sha1" // nolint: gosec 5 | "encoding/json" 6 | "fmt" 7 | "regexp" 8 | 9 | "github.com/giantswarm/apiextensions/v6/pkg/apis/core/v1alpha1" 10 | "github.com/giantswarm/certs/v4/pkg/certs" 11 | "github.com/giantswarm/microerror" 12 | ) 13 | 14 | const ( 15 | CAID = "ca" 16 | CrtID = "crt" 17 | KeyID = "key" 18 | ) 19 | 20 | func AllowBareDomains(customObject v1alpha1.CertConfig) bool { 21 | return customObject.Spec.Cert.AllowBareDomains 22 | } 23 | 24 | func AltNames(customObject v1alpha1.CertConfig) []string { 25 | return customObject.Spec.Cert.AltNames 26 | } 27 | 28 | func ClusterComponent(customObject v1alpha1.CertConfig) string { 29 | return customObject.Spec.Cert.ClusterComponent 30 | } 31 | 32 | func ClusterID(customObject v1alpha1.CertConfig) string { 33 | return customObject.Spec.Cert.ClusterID 34 | } 35 | 36 | func CommonName(customObject v1alpha1.CertConfig) string { 37 | return customObject.Spec.Cert.CommonName 38 | } 39 | 40 | func ClusterNamespace(customObject v1alpha1.CertConfig) string { 41 | return ClusterID(customObject) 42 | } 43 | 44 | func CrtTTL(customObject v1alpha1.CertConfig) string { 45 | return customObject.Spec.Cert.TTL 46 | } 47 | 48 | // nolint: gosec 49 | func CustomObjectHash(customObject v1alpha1.CertConfig) (string, error) { 50 | b, err := json.Marshal(customObject.Spec.Cert) 51 | if err != nil { 52 | return "", microerror.Mask(err) 53 | } 54 | 55 | h := sha1.New() 56 | if _, err := h.Write(b); err != nil { 57 | return "", err 58 | } 59 | bs := h.Sum(nil) 60 | 61 | return fmt.Sprintf("%x", bs), nil 62 | } 63 | 64 | func IPSANs(customObject v1alpha1.CertConfig) []string { 65 | return customObject.Spec.Cert.IPSANs 66 | } 67 | 68 | func IsDeleted(customObject v1alpha1.CertConfig) bool { 69 | return customObject.GetDeletionTimestamp() != nil 70 | } 71 | 72 | func Organizations(customObject v1alpha1.CertConfig) []string { 73 | a := make([]string, 0) 74 | 75 | // See https://github.com/giantswarm/giantswarm/issues/24722 76 | // `kubectl-gs login` creates a kubeconfig with ClusterComponent set to something like 77 | // "<16 chars random base16 string>". Since the organizations field is used to calculate the name 78 | // of the PKI role on vault, this lead to the generation of one role for every kubeconfig request. 79 | // To avoid that, we want to avoid the random string to be part of the organizations. 80 | matched, err := regexp.MatchString(`^[a-f0-9]{16}$`, customObject.Spec.Cert.ClusterComponent) 81 | if customObject.Spec.Cert.ClusterComponent != "" && (len(customObject.Spec.Cert.Organizations) == 0 || err != nil || !matched) { 82 | a = append(a, customObject.Spec.Cert.ClusterComponent) 83 | } 84 | 85 | return append(a, customObject.Spec.Cert.Organizations...) 86 | } 87 | 88 | func RoleTTL(customObject v1alpha1.CertConfig) string { 89 | return customObject.Spec.Cert.TTL 90 | } 91 | 92 | func SecretName(customObject v1alpha1.CertConfig) string { 93 | cert := certs.Cert(customObject.Spec.Cert.ClusterComponent) 94 | return certs.K8sName(ClusterID(customObject), cert) 95 | } 96 | 97 | func SecretLabels(customObject v1alpha1.CertConfig) map[string]string { 98 | cert := certs.Cert(customObject.Spec.Cert.ClusterComponent) 99 | return certs.K8sLabels(ClusterID(customObject), cert) 100 | } 101 | 102 | func ToCustomObject(v interface{}) (v1alpha1.CertConfig, error) { 103 | customObjectPointer, ok := v.(*v1alpha1.CertConfig) 104 | if !ok { 105 | return v1alpha1.CertConfig{}, microerror.Maskf(wrongTypeError, "expected '%T', got '%T'", &v1alpha1.CertConfig{}, v) 106 | } 107 | customObject := *customObjectPointer 108 | 109 | return customObject, nil 110 | } 111 | -------------------------------------------------------------------------------- /service/controller/key/key_test.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "testing" 7 | 8 | "github.com/giantswarm/apiextensions/v6/pkg/apis/core/v1alpha1" 9 | ) 10 | 11 | func Test_Organization(t *testing.T) { 12 | testCases := []struct { 13 | Organizations []string 14 | ClusterComponent string 15 | ExpectedOrganizations []string 16 | ExpectedOutput []string 17 | }{ 18 | { 19 | Organizations: []string{}, 20 | ClusterComponent: "api", 21 | ExpectedOrganizations: []string{}, 22 | ExpectedOutput: []string{"api"}, 23 | }, 24 | { 25 | Organizations: []string{"system:master"}, 26 | ClusterComponent: "api", 27 | ExpectedOrganizations: []string{"system:master"}, 28 | ExpectedOutput: []string{"api", "system:master"}, 29 | }, 30 | { 31 | Organizations: []string{"system:master", "giantswarm"}, 32 | ClusterComponent: "api", 33 | ExpectedOrganizations: []string{"system:master", "giantswarm"}, 34 | ExpectedOutput: []string{"api", "giantswarm", "system:master"}, 35 | }, 36 | { 37 | Organizations: []string{"giantswarm", "system:master"}, 38 | ClusterComponent: "api", 39 | ExpectedOrganizations: []string{"giantswarm", "system:master"}, 40 | ExpectedOutput: []string{"api", "giantswarm", "system:master"}, 41 | }, 42 | } 43 | 44 | for i, tc := range testCases { 45 | customObject := v1alpha1.CertConfig{ 46 | Spec: v1alpha1.CertConfigSpec{ 47 | Cert: v1alpha1.CertConfigSpecCert{ 48 | ClusterComponent: tc.ClusterComponent, 49 | Organizations: tc.Organizations, 50 | }, 51 | }, 52 | } 53 | 54 | for j := 0; j < 10; j++ { 55 | if !reflect.DeepEqual(tc.ExpectedOrganizations, customObject.Spec.Cert.Organizations) { 56 | t.Fatalf("case %d iteration %d expected %#v got %#v", i, j, tc.ExpectedOrganizations, customObject.Spec.Cert.Organizations) 57 | } 58 | 59 | Organizations(customObject) 60 | 61 | result := Organizations(customObject) 62 | sort.Strings(result) 63 | 64 | if !reflect.DeepEqual(tc.ExpectedOutput, result) { 65 | t.Fatalf("case %d iteration %d expected %#v got %#v", i, j, tc.ExpectedOutput, result) 66 | } 67 | } 68 | } 69 | } 70 | 71 | func TestOrganizationCapacity(t *testing.T) { 72 | // create a slice of capacity greater than the number of elements 73 | // that the copy is going to have 74 | orgs := make([]string, 1, 4) 75 | orgs[0] = "myorg" 76 | 77 | customObject := v1alpha1.CertConfig{ 78 | Spec: v1alpha1.CertConfigSpec{ 79 | Cert: v1alpha1.CertConfigSpecCert{ 80 | ClusterComponent: "api", 81 | Organizations: orgs, 82 | }, 83 | }, 84 | } 85 | 86 | // here create an extended copy of orgs 87 | o := Organizations(customObject) 88 | 89 | // call sort on the copy, this will create havok in the original 90 | sort.Strings(o) 91 | 92 | expected := "myorg" 93 | actual := customObject.Spec.Cert.Organizations[0] 94 | if expected != actual { 95 | t.Errorf("customObject organizations changed by sorting an unrelated slice, expected %s, actual %s", expected, actual) 96 | } 97 | } 98 | 99 | func TestClusterComponent(t *testing.T) { 100 | expectedClusterComponent := "calico" 101 | 102 | obj := v1alpha1.CertConfig{ 103 | Spec: v1alpha1.CertConfigSpec{ 104 | Cert: v1alpha1.CertConfigSpecCert{ 105 | ClusterComponent: "calico", 106 | }, 107 | }, 108 | } 109 | 110 | if ClusterComponent(obj) != expectedClusterComponent { 111 | t.Fatalf("clusterComponent %#q, want %s", ClusterComponent(obj), expectedClusterComponent) 112 | } 113 | } 114 | 115 | func TestOrganizations(t *testing.T) { 116 | tests := []struct { 117 | name string 118 | customObject v1alpha1.CertConfig 119 | want []string 120 | }{ 121 | { 122 | name: "Single cluster component", 123 | customObject: v1alpha1.CertConfig{ 124 | Spec: v1alpha1.CertConfigSpec{ 125 | Cert: v1alpha1.CertConfigSpecCert{ 126 | ClusterComponent: "etcd", 127 | Organizations: nil, 128 | }, 129 | }, 130 | }, 131 | want: []string{"etcd"}, 132 | }, 133 | { 134 | name: "Cluster component and legit organization", 135 | customObject: v1alpha1.CertConfig{ 136 | Spec: v1alpha1.CertConfigSpec{ 137 | Cert: v1alpha1.CertConfigSpecCert{ 138 | ClusterComponent: "etcd", 139 | Organizations: []string{"system:masters"}, 140 | }, 141 | }, 142 | }, 143 | want: []string{"etcd", "system:masters"}, 144 | }, 145 | { 146 | name: "Random cluster component and legit organization", 147 | customObject: v1alpha1.CertConfig{ 148 | Spec: v1alpha1.CertConfigSpec{ 149 | Cert: v1alpha1.CertConfigSpecCert{ 150 | ClusterComponent: "73ce775d904da53e", 151 | Organizations: []string{"system:masters"}, 152 | }, 153 | }, 154 | }, 155 | want: []string{"system:masters"}, 156 | }, 157 | { 158 | name: "Random cluster component but no organization", 159 | customObject: v1alpha1.CertConfig{ 160 | Spec: v1alpha1.CertConfigSpec{ 161 | Cert: v1alpha1.CertConfigSpecCert{ 162 | ClusterComponent: "73ce775d904da53e", 163 | Organizations: nil, 164 | }, 165 | }, 166 | }, 167 | want: []string{"73ce775d904da53e"}, 168 | }, 169 | { 170 | name: "Empty cluster component", 171 | customObject: v1alpha1.CertConfig{ 172 | Spec: v1alpha1.CertConfigSpec{ 173 | Cert: v1alpha1.CertConfigSpecCert{ 174 | ClusterComponent: "", 175 | Organizations: []string{"system:masters"}, 176 | }, 177 | }, 178 | }, 179 | want: []string{"system:masters"}, 180 | }, 181 | } 182 | for _, tt := range tests { 183 | t.Run(tt.name, func(t *testing.T) { 184 | if got := Organizations(tt.customObject); !reflect.DeepEqual(got, tt.want) { 185 | t.Errorf("Organizations() = %v, want %v", got, tt.want) 186 | } 187 | }) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /service/controller/resource_set.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/giantswarm/microerror" 7 | "github.com/giantswarm/micrologger" 8 | "github.com/giantswarm/operatorkit/v7/pkg/resource" 9 | "github.com/giantswarm/operatorkit/v7/pkg/resource/crud" 10 | "github.com/giantswarm/operatorkit/v7/pkg/resource/wrapper/metricsresource" 11 | "github.com/giantswarm/operatorkit/v7/pkg/resource/wrapper/retryresource" 12 | "github.com/giantswarm/vaultcrt" 13 | "github.com/giantswarm/vaultpki" 14 | "github.com/giantswarm/vaultrole" 15 | vaultapi "github.com/hashicorp/vault/api" 16 | "k8s.io/client-go/kubernetes" 17 | "sigs.k8s.io/controller-runtime/pkg/client" 18 | 19 | "github.com/giantswarm/cert-operator/v3/service/controller/resources/vaultaccess" 20 | vaultcrtresource "github.com/giantswarm/cert-operator/v3/service/controller/resources/vaultcrt" 21 | vaultpkiresource "github.com/giantswarm/cert-operator/v3/service/controller/resources/vaultpki" 22 | vaultroleresource "github.com/giantswarm/cert-operator/v3/service/controller/resources/vaultrole" 23 | ) 24 | 25 | type ResourceSetConfig struct { 26 | K8sClient kubernetes.Interface 27 | CtrlClient client.Client 28 | Logger micrologger.Logger 29 | VaultClient *vaultapi.Client 30 | VaultCrt vaultcrt.Interface 31 | VaultPKI vaultpki.Interface 32 | VaultRole vaultrole.Interface 33 | 34 | ExpirationThreshold time.Duration 35 | Namespace string 36 | ProjectName string 37 | } 38 | 39 | func NewResourceSet(config ResourceSetConfig) ([]resource.Interface, error) { 40 | var err error 41 | 42 | var vaultAccessResource resource.Interface 43 | { 44 | c := vaultaccess.Config{ 45 | Logger: config.Logger, 46 | VaultClient: config.VaultClient, 47 | } 48 | 49 | vaultAccessResource, err = vaultaccess.New(c) 50 | if err != nil { 51 | return nil, microerror.Mask(err) 52 | } 53 | } 54 | 55 | var vaultCrtResource resource.Interface 56 | { 57 | c := vaultcrtresource.Config{ 58 | CurrentTimeFactory: func() time.Time { return time.Now() }, 59 | K8sClient: config.K8sClient, 60 | CtrlClient: config.CtrlClient, 61 | Logger: config.Logger, 62 | VaultCrt: config.VaultCrt, 63 | 64 | ExpirationThreshold: config.ExpirationThreshold, 65 | Namespace: config.Namespace, 66 | } 67 | 68 | ops, err := vaultcrtresource.New(c) 69 | if err != nil { 70 | return nil, microerror.Mask(err) 71 | } 72 | 73 | vaultCrtResource, err = toCRUDResource(config.Logger, ops) 74 | if err != nil { 75 | return nil, microerror.Mask(err) 76 | } 77 | } 78 | 79 | var vaultPKIResource resource.Interface 80 | { 81 | c := vaultpkiresource.Config{ 82 | Logger: config.Logger, 83 | VaultPKI: config.VaultPKI, 84 | } 85 | 86 | ops, err := vaultpkiresource.New(c) 87 | if err != nil { 88 | return nil, microerror.Mask(err) 89 | } 90 | 91 | vaultPKIResource, err = toCRUDResource(config.Logger, ops) 92 | if err != nil { 93 | return nil, microerror.Mask(err) 94 | } 95 | } 96 | 97 | var vaultRoleResource resource.Interface 98 | { 99 | c := vaultroleresource.Config{ 100 | Logger: config.Logger, 101 | VaultRole: config.VaultRole, 102 | } 103 | 104 | ops, err := vaultroleresource.New(c) 105 | if err != nil { 106 | return nil, microerror.Mask(err) 107 | } 108 | 109 | vaultRoleResource, err = toCRUDResource(config.Logger, ops) 110 | if err != nil { 111 | return nil, microerror.Mask(err) 112 | } 113 | } 114 | 115 | resources := []resource.Interface{ 116 | vaultAccessResource, 117 | vaultPKIResource, 118 | vaultRoleResource, 119 | vaultCrtResource, 120 | } 121 | 122 | { 123 | c := retryresource.WrapConfig{ 124 | Logger: config.Logger, 125 | } 126 | 127 | resources, err = retryresource.Wrap(resources, c) 128 | if err != nil { 129 | return nil, microerror.Mask(err) 130 | } 131 | } 132 | 133 | { 134 | c := metricsresource.WrapConfig{} 135 | 136 | resources, err = metricsresource.Wrap(resources, c) 137 | if err != nil { 138 | return nil, microerror.Mask(err) 139 | } 140 | } 141 | 142 | return resources, nil 143 | } 144 | 145 | func toCRUDResource(logger micrologger.Logger, v crud.Interface) (*crud.Resource, error) { 146 | c := crud.ResourceConfig{ 147 | CRUD: v, 148 | Logger: logger, 149 | } 150 | 151 | r, err := crud.NewResource(c) 152 | if err != nil { 153 | return nil, microerror.Mask(err) 154 | } 155 | 156 | return r, nil 157 | } 158 | -------------------------------------------------------------------------------- /service/controller/resources/vaultaccess/create.go: -------------------------------------------------------------------------------- 1 | package vaultaccess 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/giantswarm/microerror" 7 | "github.com/giantswarm/operatorkit/v7/pkg/controller/context/reconciliationcanceledcontext" 8 | ) 9 | 10 | func (r *Resource) EnsureCreated(ctx context.Context, obj interface{}) error { 11 | r.logger.LogCtx(ctx, "level", "debug", "message", "renewing the Vault token") 12 | _, err := r.vaultClient.Auth().Token().RenewSelf(0) 13 | if IsVaultAccess(err) { 14 | r.logger.LogCtx(ctx, "level", "debug", "message", "vault not reachable") 15 | r.logger.LogCtx(ctx, "level", "debug", "message", "vault upgrade in progress") 16 | r.logger.LogCtx(ctx, "level", "debug", "message", "canceling reconciliation") 17 | reconciliationcanceledcontext.SetCanceled(ctx) 18 | return nil 19 | 20 | } else if err != nil { 21 | return microerror.Mask(err) 22 | } 23 | r.logger.LogCtx(ctx, "level", "debug", "message", "renewed the Vault token") 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /service/controller/resources/vaultaccess/delete.go: -------------------------------------------------------------------------------- 1 | package vaultaccess 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/giantswarm/microerror" 7 | "github.com/giantswarm/operatorkit/v7/pkg/controller/context/reconciliationcanceledcontext" 8 | ) 9 | 10 | func (r *Resource) EnsureDeleted(ctx context.Context, obj interface{}) error { 11 | _, err := r.vaultClient.Auth().Token().LookupSelf() 12 | if IsVaultAccess(err) { 13 | r.logger.LogCtx(ctx, "level", "debug", "message", "vault not reachable") 14 | r.logger.LogCtx(ctx, "level", "debug", "message", "vault upgrade in progress") 15 | r.logger.LogCtx(ctx, "level", "debug", "message", "canceling reconciliation") 16 | reconciliationcanceledcontext.SetCanceled(ctx) 17 | return nil 18 | 19 | } else if err != nil { 20 | return microerror.Mask(err) 21 | } 22 | 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /service/controller/resources/vaultaccess/error.go: -------------------------------------------------------------------------------- 1 | package vaultaccess 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/giantswarm/microerror" 7 | ) 8 | 9 | var invalidConfigError = µerror.Error{ 10 | Kind: "invalidConfigError", 11 | } 12 | 13 | // IsInvalidConfig asserts invalidConfigError. 14 | func IsInvalidConfig(err error) bool { 15 | return microerror.Cause(err) == invalidConfigError 16 | } 17 | 18 | var vaultAccessError = µerror.Error{ 19 | Kind: "vaultAccessError", 20 | } 21 | 22 | // IsVaultAccess asserts vaultAccessError. The matcher also asserts errors 23 | // caused by situations in which Vault is updated strategically and thus 24 | // temporarily replies with HTTP responses. In such cases we intend to cancel 25 | // reconciliation and wait until Vault is fully operational again. 26 | // 27 | // Get https://vault.g8s.amag.ch:8200/v1/sys/mounts: http: server gave HTTP response to HTTPS client 28 | func IsVaultAccess(err error) bool { 29 | if err == nil { 30 | return false 31 | } 32 | 33 | c := microerror.Cause(err) 34 | 35 | if strings.Contains(c.Error(), "server gave HTTP response to HTTPS client") { 36 | return true 37 | } 38 | 39 | if c == vaultAccessError { 40 | return true 41 | } 42 | 43 | return false 44 | } 45 | -------------------------------------------------------------------------------- /service/controller/resources/vaultaccess/resource.go: -------------------------------------------------------------------------------- 1 | package vaultaccess 2 | 3 | import ( 4 | "github.com/giantswarm/microerror" 5 | "github.com/giantswarm/micrologger" 6 | vaultapi "github.com/hashicorp/vault/api" 7 | ) 8 | 9 | const ( 10 | Name = "vaultaccess" 11 | ) 12 | 13 | type Config struct { 14 | Logger micrologger.Logger 15 | VaultClient *vaultapi.Client 16 | } 17 | 18 | type Resource struct { 19 | logger micrologger.Logger 20 | vaultClient *vaultapi.Client 21 | } 22 | 23 | func New(config Config) (*Resource, error) { 24 | if config.Logger == nil { 25 | return nil, microerror.Maskf(invalidConfigError, "%T.Logger must not be empty", config) 26 | } 27 | if config.VaultClient == nil { 28 | return nil, microerror.Maskf(invalidConfigError, "%T.VaultClient must not be empty", config) 29 | } 30 | 31 | r := &Resource{ 32 | logger: config.Logger, 33 | vaultClient: config.VaultClient, 34 | } 35 | 36 | return r, nil 37 | } 38 | 39 | func (r *Resource) Name() string { 40 | return Name 41 | } 42 | -------------------------------------------------------------------------------- /service/controller/resources/vaultcrt/create.go: -------------------------------------------------------------------------------- 1 | package vaultcrt 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/giantswarm/microerror" 7 | apiv1 "k8s.io/api/core/v1" 8 | apimeta "k8s.io/apimachinery/pkg/api/meta" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/types" 11 | capi "sigs.k8s.io/cluster-api/api/v1beta1" 12 | 13 | "github.com/giantswarm/cert-operator/v3/service/controller/key" 14 | ) 15 | 16 | func (r *Resource) ApplyCreateChange(ctx context.Context, obj, createChange interface{}) error { 17 | secretToCreate, err := toSecret(createChange) 18 | if err != nil { 19 | return microerror.Mask(err) 20 | } 21 | 22 | if secretToCreate != nil { 23 | customObject, err := key.ToCustomObject(obj) 24 | if err != nil { 25 | return microerror.Mask(err) 26 | } 27 | 28 | r.logger.LogCtx(ctx, "level", "debug", "message", "finding cluster resource") 29 | cluster := &capi.Cluster{} 30 | err = r.ctrlClient.Get(ctx, types.NamespacedName{ 31 | Namespace: customObject.Namespace, 32 | Name: key.ClusterID(customObject)}, 33 | cluster) 34 | if apimeta.IsNoMatchError(err) { 35 | // fall through if the cluster CRD is not installed. This is the case in KVM installations, we ignore them for now. 36 | } else if err != nil { 37 | return microerror.Maskf(notFoundError, "Could not find cluster %s in namespace %s.", 38 | key.ClusterID(customObject), 39 | customObject.Namespace) 40 | } 41 | 42 | r.logger.LogCtx(ctx, "level", "debug", "message", "creating the secret in the Kubernetes API") 43 | 44 | _, err = r.k8sClient.CoreV1().Secrets(customObject.GetNamespace()).Create(ctx, secretToCreate, metav1.CreateOptions{}) 45 | if err != nil { 46 | return microerror.Mask(err) 47 | } 48 | 49 | r.logger.LogCtx(ctx, "level", "debug", "message", "created the secret in the Kubernetes API") 50 | } else { 51 | r.logger.LogCtx(ctx, "level", "debug", "message", "the secret does not need to be created in the Kubernetes API") 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func (r *Resource) newCreateChange(ctx context.Context, obj, currentState, desiredState interface{}) (interface{}, error) { 58 | customObject, err := key.ToCustomObject(obj) 59 | if err != nil { 60 | return nil, microerror.Mask(err) 61 | } 62 | currentSecret, err := toSecret(currentState) 63 | if err != nil { 64 | return nil, microerror.Mask(err) 65 | } 66 | desiredSecret, err := toSecret(desiredState) 67 | if err != nil { 68 | return nil, microerror.Mask(err) 69 | } 70 | 71 | r.logger.LogCtx(ctx, "level", "debug", "message", "finding out if the secret has to be created") 72 | 73 | var secretToCreate *apiv1.Secret 74 | if currentSecret == nil { 75 | ca, crt, k, err := r.issueCertificate(customObject) 76 | if err != nil { 77 | return nil, microerror.Mask(err) 78 | } 79 | 80 | secretToCreate = desiredSecret 81 | secretToCreate.StringData[key.CAID] = ca 82 | secretToCreate.StringData[key.CrtID] = crt 83 | secretToCreate.StringData[key.KeyID] = k 84 | } 85 | 86 | r.logger.LogCtx(ctx, "level", "debug", "message", "found out if the secret has to be created") 87 | 88 | return secretToCreate, nil 89 | } 90 | -------------------------------------------------------------------------------- /service/controller/resources/vaultcrt/create_test.go: -------------------------------------------------------------------------------- 1 | package vaultcrt 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/giantswarm/apiextensions/v6/pkg/apis/core/v1alpha1" 10 | "github.com/giantswarm/micrologger/microloggertest" 11 | "github.com/giantswarm/vaultcrt/vaultcrttest" 12 | apiv1 "k8s.io/api/core/v1" 13 | apismetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | "k8s.io/client-go/kubernetes/fake" 16 | capi "sigs.k8s.io/cluster-api/api/v1beta1" 17 | fakectrl "sigs.k8s.io/controller-runtime/pkg/client/fake" //nolint:staticcheck // v0.6.4 has a deprecation on pkg/client/fake that was removed in later versions 18 | ) 19 | 20 | func Test_Resource_VaultCrt_newCreateChange(t *testing.T) { 21 | testCases := []struct { 22 | Obj interface{} 23 | CurrentState interface{} 24 | DesiredState interface{} 25 | ExpectedSecret *apiv1.Secret 26 | }{ 27 | // Test 0 ensures a non-nil current state results in the create state to be 28 | // empty. 29 | { 30 | Obj: &v1alpha1.CertConfig{ 31 | Spec: v1alpha1.CertConfigSpec{ 32 | Cert: v1alpha1.CertConfigSpecCert{ 33 | ClusterID: "foobar", 34 | }, 35 | }, 36 | }, 37 | CurrentState: &apiv1.Secret{}, 38 | DesiredState: &apiv1.Secret{}, 39 | ExpectedSecret: nil, 40 | }, 41 | 42 | // Test 1 is the same 1 but with different content for the current state. 43 | { 44 | Obj: &v1alpha1.CertConfig{ 45 | Spec: v1alpha1.CertConfigSpec{ 46 | Cert: v1alpha1.CertConfigSpecCert{ 47 | ClusterID: "foobar", 48 | }, 49 | }, 50 | }, 51 | CurrentState: &apiv1.Secret{ 52 | ObjectMeta: apismetav1.ObjectMeta{ 53 | Name: "foobar-api", 54 | Labels: map[string]string{ 55 | "clusterID": "foobar", 56 | "clusterComponent": "api", 57 | }, 58 | }, 59 | StringData: map[string]string{ 60 | "ca": "", 61 | "crt": "", 62 | "key": "", 63 | }, 64 | }, 65 | DesiredState: &apiv1.Secret{}, 66 | ExpectedSecret: nil, 67 | }, 68 | 69 | // Test 2 ensures an empty current state results in a create state that 70 | // equals the desired state. NOTE that the secret data is extended with 71 | // actual certificate content, which in this case is some fake content from 72 | // the fake VaultCrt service. 73 | { 74 | Obj: &v1alpha1.CertConfig{ 75 | Spec: v1alpha1.CertConfigSpec{ 76 | Cert: v1alpha1.CertConfigSpecCert{ 77 | ClusterID: "foobar", 78 | }, 79 | }, 80 | }, 81 | CurrentState: nil, 82 | DesiredState: &apiv1.Secret{ 83 | ObjectMeta: apismetav1.ObjectMeta{ 84 | Name: "foobar-api", 85 | Labels: map[string]string{ 86 | "clusterID": "foobar", 87 | "clusterComponent": "api", 88 | }, 89 | }, 90 | StringData: map[string]string{ 91 | "ca": "", 92 | "crt": "", 93 | "key": "", 94 | }, 95 | }, 96 | ExpectedSecret: &apiv1.Secret{ 97 | ObjectMeta: apismetav1.ObjectMeta{ 98 | Name: "foobar-api", 99 | Labels: map[string]string{ 100 | "clusterID": "foobar", 101 | "clusterComponent": "api", 102 | }, 103 | }, 104 | StringData: map[string]string{ 105 | "ca": "test CA", 106 | "crt": "test crt", 107 | "key": "test key", 108 | }, 109 | }, 110 | }, 111 | } 112 | 113 | var err error 114 | var newResource *Resource 115 | { 116 | c := DefaultConfig() 117 | scheme := runtime.NewScheme() 118 | _ = capi.AddToScheme(scheme) 119 | 120 | c.CurrentTimeFactory = func() time.Time { return time.Time{} } 121 | c.K8sClient = fake.NewSimpleClientset() 122 | c.CtrlClient = fakectrl.NewClientBuilder().WithScheme(scheme).Build() 123 | c.Logger = microloggertest.New() 124 | c.VaultCrt = vaultcrttest.New() 125 | 126 | c.ExpirationThreshold = 24 * time.Hour 127 | c.Namespace = "default" // nolint: goconst 128 | 129 | newResource, err = New(c) 130 | if err != nil { 131 | t.Fatal("expected", nil, "got", err) 132 | } 133 | } 134 | 135 | for i, tc := range testCases { 136 | result, err := newResource.newCreateChange(context.TODO(), tc.Obj, tc.CurrentState, tc.DesiredState) 137 | if err != nil { 138 | t.Fatal("case", i, "expected", nil, "got", err) 139 | } 140 | secret := result.(*apiv1.Secret) 141 | if !reflect.DeepEqual(tc.ExpectedSecret, secret) { 142 | t.Fatalf("case %d expected %#v got %#v", i, tc.ExpectedSecret, secret) 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /service/controller/resources/vaultcrt/current.go: -------------------------------------------------------------------------------- 1 | package vaultcrt 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/giantswarm/apiextensions/v6/pkg/apis/core/v1alpha1" 9 | "github.com/giantswarm/certs/v4/pkg/certs" 10 | "github.com/giantswarm/microerror" 11 | "github.com/giantswarm/operatorkit/v7/pkg/controller/context/finalizerskeptcontext" 12 | "github.com/giantswarm/operatorkit/v7/pkg/controller/context/resourcecanceledcontext" 13 | "github.com/prometheus/client_golang/prometheus" 14 | corev1 "k8s.io/api/core/v1" 15 | apierrors "k8s.io/apimachinery/pkg/api/errors" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | 18 | "github.com/giantswarm/cert-operator/v3/pkg/label" 19 | "github.com/giantswarm/cert-operator/v3/service/controller/key" 20 | ) 21 | 22 | func (r *Resource) GetCurrentState(ctx context.Context, obj interface{}) (interface{}, error) { 23 | customObject, err := key.ToCustomObject(obj) 24 | if err != nil { 25 | return nil, microerror.Mask(err) 26 | } 27 | 28 | r.logger.LogCtx(ctx, "level", "debug", "message", "looking for the secret in the Kubernetes API") 29 | 30 | var secret *corev1.Secret 31 | { 32 | manifest, err := r.k8sClient.CoreV1().Secrets(customObject.GetNamespace()).Get(ctx, key.SecretName(customObject), metav1.GetOptions{}) 33 | if apierrors.IsNotFound(err) { 34 | r.logger.LogCtx(ctx, "level", "debug", "message", "did not find the secret in the Kubernetes API") 35 | // fall through 36 | } else if err != nil { 37 | return nil, microerror.Mask(err) 38 | } else { 39 | r.logger.LogCtx(ctx, "level", "debug", "message", "found the secret in the Kubernetes API") 40 | secret = manifest 41 | r.updateVersionGauge(ctx, customObject, versionGauge, secret) 42 | } 43 | } 44 | 45 | // In case a cluster deletion happens, we want to delete all secrets holding 46 | // certificates. We still need the certificates for draining nodes on KVM 47 | // though. So as long as pods are there we delay the deletion of the secrets 48 | // here in order to still use them in the kvm-operator. The impact of this for 49 | // AWS and Azure is zero, because when listing on namespaces that do not exist 50 | // we get an empty list and thus do nothing here. For KVM, as soon as the 51 | // draining was done and the pods got removed we get an empty list here after 52 | // the delete event got replayed. Then we just remove the secrets as usual. 53 | if key.IsDeleted(customObject) { 54 | // If this customObject is not the cert we are supporting in certs library, 55 | // we don't need to check for running pods. 56 | if !r.checkCertType(customObject) { 57 | r.logger.LogCtx(ctx, "level", "debug", "message", fmt.Sprintf("unsupported cert type %#q", key.ClusterComponent(customObject))) 58 | return secret, nil 59 | } 60 | 61 | n := key.ClusterNamespace(customObject) 62 | list, err := r.k8sClient.CoreV1().Pods(n).List(ctx, metav1.ListOptions{}) 63 | if err != nil { 64 | return nil, microerror.Mask(err) 65 | } 66 | if len(list.Items) != 0 { 67 | r.logger.LogCtx(ctx, "level", "debug", "message", "cannot finish deletion of the secret due to existing pods") 68 | resourcecanceledcontext.SetCanceled(ctx) 69 | finalizerskeptcontext.SetKept(ctx) 70 | r.logger.LogCtx(ctx, "level", "debug", "message", "canceling resource for custom object") 71 | 72 | return nil, nil 73 | } 74 | } 75 | 76 | return secret, nil 77 | } 78 | 79 | // checkCertType checks whether customObject is one of the Cert types we are supporting in certs library. 80 | func (r *Resource) checkCertType(customObject v1alpha1.CertConfig) bool { 81 | c := certs.Cert(key.ClusterComponent(customObject)) 82 | for _, cert := range certs.AllCerts { 83 | if cert == c { 84 | return true 85 | } 86 | } 87 | return false 88 | } 89 | 90 | func (r *Resource) updateVersionGauge(ctx context.Context, customObject v1alpha1.CertConfig, gauge *prometheus.GaugeVec, secret *corev1.Secret) { 91 | version, ok := secret.Labels[label.OperatorVersion] 92 | if !ok { 93 | r.logger.LogCtx(ctx, "level", "warning", "message", fmt.Sprintf("cannot update current version bundle version metric: label '%s' must not be empty", label.OperatorVersion)) 94 | return 95 | } 96 | 97 | split := strings.Split(version, ".") 98 | if len(split) != 3 { 99 | r.logger.LogCtx(ctx, "level", "warning", "message", fmt.Sprintf("cannot update current version metric: invalid version format, expected '..', got '%s'", version)) 100 | return 101 | } 102 | 103 | major := split[0] 104 | minor := split[1] 105 | patch := split[2] 106 | 107 | gauge.WithLabelValues(major, minor, patch).Set(1) 108 | } 109 | -------------------------------------------------------------------------------- /service/controller/resources/vaultcrt/delete.go: -------------------------------------------------------------------------------- 1 | package vaultcrt 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/giantswarm/microerror" 7 | "github.com/giantswarm/operatorkit/v7/pkg/resource/crud" 8 | apiv1 "k8s.io/api/core/v1" 9 | apierrors "k8s.io/apimachinery/pkg/api/errors" 10 | apismetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | 12 | "github.com/giantswarm/cert-operator/v3/service/controller/key" 13 | ) 14 | 15 | func (r *Resource) ApplyDeleteChange(ctx context.Context, obj, deleteChange interface{}) error { 16 | secretToDelete, err := toSecret(deleteChange) 17 | if err != nil { 18 | return microerror.Mask(err) 19 | } 20 | 21 | if secretToDelete != nil { 22 | customObject, err := key.ToCustomObject(obj) 23 | if err != nil { 24 | return microerror.Mask(err) 25 | } 26 | r.logger.LogCtx(ctx, "level", "debug", "message", "deleting the sercet in the Kubernetes API") 27 | 28 | err = r.k8sClient.CoreV1().Secrets(customObject.GetNamespace()).Delete(ctx, secretToDelete.Name, apismetav1.DeleteOptions{}) 29 | if apierrors.IsNotFound(err) { 30 | // fall through 31 | } else if err != nil { 32 | return microerror.Mask(err) 33 | } 34 | 35 | r.logger.LogCtx(ctx, "level", "debug", "message", "deleted the sercet in the Kubernetes API") 36 | } else { 37 | r.logger.LogCtx(ctx, "level", "debug", "message", "the sercet does not need to be deleted from the Kubernetes API") 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func (r *Resource) NewDeletePatch(ctx context.Context, obj, currentState, desiredState interface{}) (*crud.Patch, error) { 44 | delete, err := r.newDeleteChange(ctx, obj, currentState, desiredState) 45 | if err != nil { 46 | return nil, microerror.Mask(err) 47 | } 48 | 49 | patch := crud.NewPatch() 50 | patch.SetDeleteChange(delete) 51 | 52 | return patch, nil 53 | } 54 | 55 | func (r *Resource) newDeleteChange(ctx context.Context, obj, currentState, desiredState interface{}) (interface{}, error) { 56 | currentSecret, err := toSecret(currentState) 57 | if err != nil { 58 | return nil, microerror.Mask(err) 59 | } 60 | 61 | r.logger.LogCtx(ctx, "level", "debug", "message", "finding out if the secret has to be deleted") 62 | 63 | var secretToDelete *apiv1.Secret 64 | if currentSecret != nil { 65 | secretToDelete = currentSecret 66 | } 67 | 68 | r.logger.LogCtx(ctx, "level", "debug", "message", "found out if the secret has to be deleted") 69 | 70 | return secretToDelete, nil 71 | } 72 | -------------------------------------------------------------------------------- /service/controller/resources/vaultcrt/delete_test.go: -------------------------------------------------------------------------------- 1 | package vaultcrt 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/giantswarm/apiextensions/v6/pkg/apis/core/v1alpha1" 10 | "github.com/giantswarm/micrologger/microloggertest" 11 | "github.com/giantswarm/vaultcrt/vaultcrttest" 12 | apiv1 "k8s.io/api/core/v1" 13 | apismetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | "k8s.io/client-go/kubernetes/fake" 16 | capi "sigs.k8s.io/cluster-api/api/v1beta1" 17 | fakectrl "sigs.k8s.io/controller-runtime/pkg/client/fake" //nolint:staticcheck // v0.6.4 has a deprecation on pkg/client/fake that was removed in later versions 18 | ) 19 | 20 | func Test_Resource_VaultCrt_newDeleteChange(t *testing.T) { 21 | testCases := []struct { 22 | Obj interface{} 23 | CurrentState interface{} 24 | DesiredState interface{} 25 | ExpectedSecret *apiv1.Secret 26 | }{ 27 | // Test 0 ensures that zero value input results in zero value output. 28 | { 29 | Obj: &v1alpha1.CertConfig{ 30 | Spec: v1alpha1.CertConfigSpec{ 31 | Cert: v1alpha1.CertConfigSpecCert{ 32 | ClusterID: "foobar", 33 | }, 34 | }, 35 | }, 36 | CurrentState: nil, 37 | DesiredState: &apiv1.Secret{}, 38 | ExpectedSecret: nil, 39 | }, 40 | 41 | // Test 1 is the same as 0 but with initialized empty pointer values. 42 | { 43 | Obj: &v1alpha1.CertConfig{ 44 | Spec: v1alpha1.CertConfigSpec{ 45 | Cert: v1alpha1.CertConfigSpecCert{ 46 | ClusterID: "foobar", 47 | }, 48 | }, 49 | }, 50 | CurrentState: &apiv1.Secret{}, 51 | DesiredState: &apiv1.Secret{}, 52 | ExpectedSecret: &apiv1.Secret{}, 53 | }, 54 | 55 | // Test 2 ensures that the delete state is defined by the current state 56 | // since we want to remove the current state in case a delete event happens. 57 | { 58 | Obj: &v1alpha1.CertConfig{ 59 | Spec: v1alpha1.CertConfigSpec{ 60 | Cert: v1alpha1.CertConfigSpecCert{ 61 | ClusterID: "foobar", 62 | }, 63 | }, 64 | }, 65 | CurrentState: &apiv1.Secret{ 66 | ObjectMeta: apismetav1.ObjectMeta{ 67 | Name: "al9qy-worker", 68 | Labels: map[string]string{ 69 | "clusterID": "al9qy", 70 | "clusterComponent": "worker", 71 | }, 72 | }, 73 | StringData: map[string]string{ 74 | "ca": "", 75 | "crt": "", 76 | "key": "", 77 | }, 78 | }, 79 | DesiredState: &apiv1.Secret{}, 80 | ExpectedSecret: &apiv1.Secret{ 81 | ObjectMeta: apismetav1.ObjectMeta{ 82 | Name: "al9qy-worker", 83 | Labels: map[string]string{ 84 | "clusterID": "al9qy", 85 | "clusterComponent": "worker", 86 | }, 87 | }, 88 | StringData: map[string]string{ 89 | "ca": "", 90 | "crt": "", 91 | "key": "", 92 | }, 93 | }, 94 | }, 95 | } 96 | 97 | var err error 98 | var newResource *Resource 99 | { 100 | c := DefaultConfig() 101 | scheme := runtime.NewScheme() 102 | _ = capi.AddToScheme(scheme) 103 | 104 | c.CurrentTimeFactory = func() time.Time { return time.Time{} } 105 | c.K8sClient = fake.NewSimpleClientset() 106 | c.CtrlClient = fakectrl.NewClientBuilder().WithScheme(scheme).Build() 107 | c.Logger = microloggertest.New() 108 | c.VaultCrt = vaultcrttest.New() 109 | 110 | c.ExpirationThreshold = 24 * time.Hour 111 | c.Namespace = "default" 112 | 113 | newResource, err = New(c) 114 | if err != nil { 115 | t.Fatal("expected", nil, "got", err) 116 | } 117 | } 118 | 119 | for i, tc := range testCases { 120 | result, err := newResource.newDeleteChange(context.TODO(), tc.Obj, tc.CurrentState, tc.DesiredState) 121 | if err != nil { 122 | t.Fatal("case", i, "expected", nil, "got", err) 123 | } 124 | secret := result.(*apiv1.Secret) 125 | if !reflect.DeepEqual(tc.ExpectedSecret, secret) { 126 | t.Fatalf("case %d expected %#v got %#v", i, tc.ExpectedSecret, secret) 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /service/controller/resources/vaultcrt/desired.go: -------------------------------------------------------------------------------- 1 | package vaultcrt 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/giantswarm/microerror" 8 | apiv1 "k8s.io/api/core/v1" 9 | apismetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | 11 | "github.com/giantswarm/cert-operator/v3/pkg/label" 12 | "github.com/giantswarm/cert-operator/v3/pkg/project" 13 | "github.com/giantswarm/cert-operator/v3/service/controller/key" 14 | ) 15 | 16 | func (r *Resource) GetDesiredState(ctx context.Context, obj interface{}) (interface{}, error) { 17 | customObject, err := key.ToCustomObject(obj) 18 | if err != nil { 19 | return nil, microerror.Mask(err) 20 | } 21 | 22 | r.logger.LogCtx(ctx, "level", "debug", "message", "computing the desired secret") 23 | 24 | hash, err := key.CustomObjectHash(customObject) 25 | if err != nil { 26 | return nil, microerror.Mask(err) 27 | } 28 | 29 | // Add standard cert labels as well as our operator version 30 | labels := key.SecretLabels(customObject) 31 | labels[label.OperatorVersion] = project.Version() 32 | 33 | // NOTE that the actual secret content here is left blank because only the 34 | // issuer backend, e.g. Vault, can generate certificates. This has to be 35 | // considered when computing the create, delete and update state. 36 | secret := &apiv1.Secret{ 37 | ObjectMeta: apismetav1.ObjectMeta{ 38 | Name: key.SecretName(customObject), 39 | Annotations: map[string]string{ 40 | ConfigHashAnnotation: hash, 41 | UpdateTimestampAnnotation: r.currentTimeFactory().In(time.UTC).Format(UpdateTimestampLayout), 42 | }, 43 | Labels: labels, 44 | }, 45 | StringData: map[string]string{ 46 | key.CAID: "", 47 | key.CrtID: "", 48 | key.KeyID: "", 49 | }, 50 | } 51 | 52 | r.logger.LogCtx(ctx, "level", "debug", "message", "computed the desired secret") 53 | 54 | return secret, nil 55 | } 56 | -------------------------------------------------------------------------------- /service/controller/resources/vaultcrt/desired_test.go: -------------------------------------------------------------------------------- 1 | package vaultcrt 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/giantswarm/apiextensions/v6/pkg/apis/core/v1alpha1" 10 | "github.com/giantswarm/micrologger/microloggertest" 11 | "github.com/giantswarm/vaultcrt/vaultcrttest" 12 | apiv1 "k8s.io/api/core/v1" 13 | apismetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | "k8s.io/client-go/kubernetes/fake" 16 | capi "sigs.k8s.io/cluster-api/api/v1beta1" 17 | fakectrl "sigs.k8s.io/controller-runtime/pkg/client/fake" //nolint:staticcheck // v0.6.4 has a deprecation on pkg/client/fake that was removed in later versions 18 | 19 | "github.com/giantswarm/cert-operator/v3/pkg/project" 20 | ) 21 | 22 | func Test_Resource_VaultCrt_GetDesiredState(t *testing.T) { 23 | testCases := []struct { 24 | Obj interface{} 25 | ExpectedSecret *apiv1.Secret 26 | }{ 27 | // Test 0 ensures the desired state is always the same placeholder state. 28 | { 29 | Obj: &v1alpha1.CertConfig{ 30 | ObjectMeta: apismetav1.ObjectMeta{ 31 | Labels: map[string]string{ 32 | "cert-operator.giantswarm.io/version": project.Version(), 33 | }, 34 | }, 35 | Spec: v1alpha1.CertConfigSpec{ 36 | Cert: v1alpha1.CertConfigSpecCert{ 37 | ClusterID: "foobar", 38 | ClusterComponent: "api", 39 | }, 40 | }, 41 | }, 42 | ExpectedSecret: &apiv1.Secret{ 43 | ObjectMeta: apismetav1.ObjectMeta{ 44 | Name: "foobar-api", 45 | Annotations: map[string]string{ 46 | ConfigHashAnnotation: "001ad3d32b3f7d64e00ec0a3d5592fbb791849c2", 47 | UpdateTimestampAnnotation: (time.Time{}).Format(UpdateTimestampLayout), 48 | }, 49 | Labels: map[string]string{ 50 | "giantswarm.io/cluster": "foobar", 51 | "giantswarm.io/certificate": "api", 52 | "cert-operator.giantswarm.io/version": project.Version(), 53 | }, 54 | }, 55 | StringData: map[string]string{ 56 | "ca": "", 57 | "crt": "", 58 | "key": "", 59 | }, 60 | }, 61 | }, 62 | 63 | // Test 1 is the same as 0 but with a different custom object. 64 | { 65 | Obj: &v1alpha1.CertConfig{ 66 | ObjectMeta: apismetav1.ObjectMeta{ 67 | Labels: map[string]string{ 68 | "cert-operator.giantswarm.io/version": project.Version(), 69 | }, 70 | }, 71 | Spec: v1alpha1.CertConfigSpec{ 72 | Cert: v1alpha1.CertConfigSpecCert{ 73 | ClusterID: "al9qy", 74 | ClusterComponent: "worker", 75 | }, 76 | }, 77 | }, 78 | ExpectedSecret: &apiv1.Secret{ 79 | ObjectMeta: apismetav1.ObjectMeta{ 80 | Name: "al9qy-worker", 81 | Annotations: map[string]string{ 82 | ConfigHashAnnotation: "4bf7b5296ba01161f182de54b243e1400ae6660e", 83 | UpdateTimestampAnnotation: (time.Time{}).Format(UpdateTimestampLayout), 84 | }, 85 | Labels: map[string]string{ 86 | "giantswarm.io/cluster": "al9qy", 87 | "giantswarm.io/certificate": "worker", 88 | "cert-operator.giantswarm.io/version": project.Version(), 89 | }, 90 | }, 91 | StringData: map[string]string{ 92 | "ca": "", 93 | "crt": "", 94 | "key": "", 95 | }, 96 | }, 97 | }, 98 | } 99 | 100 | var err error 101 | var newResource *Resource 102 | { 103 | c := DefaultConfig() 104 | scheme := runtime.NewScheme() 105 | _ = capi.AddToScheme(scheme) 106 | 107 | c.CurrentTimeFactory = func() time.Time { return time.Time{} } 108 | c.K8sClient = fake.NewSimpleClientset() 109 | c.CtrlClient = fakectrl.NewClientBuilder().WithScheme(scheme).Build() 110 | c.Logger = microloggertest.New() 111 | c.VaultCrt = vaultcrttest.New() 112 | 113 | c.ExpirationThreshold = 24 * time.Hour 114 | c.Namespace = "default" 115 | 116 | newResource, err = New(c) 117 | if err != nil { 118 | t.Fatal("expected", nil, "got", err) 119 | } 120 | } 121 | 122 | for i, tc := range testCases { 123 | result, err := newResource.GetDesiredState(context.TODO(), tc.Obj) 124 | if err != nil { 125 | t.Fatal("case", i, "expected", nil, "got", err) 126 | } 127 | secret := result.(*apiv1.Secret) 128 | if !reflect.DeepEqual(tc.ExpectedSecret, secret) { 129 | t.Fatalf("case %d expected %#v got %#v", i, tc.ExpectedSecret, secret) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /service/controller/resources/vaultcrt/error.go: -------------------------------------------------------------------------------- 1 | package vaultcrt 2 | 3 | import ( 4 | "github.com/giantswarm/microerror" 5 | ) 6 | 7 | var missingAnnotationError = µerror.Error{ 8 | Kind: "missingAnnotationError", 9 | } 10 | 11 | // IsMissingAnnotation asserts missingAnnotationError. 12 | func IsMissingAnnotation(err error) bool { 13 | return microerror.Cause(err) == missingAnnotationError 14 | } 15 | 16 | var notFoundError = µerror.Error{ 17 | Kind: "notFoundError", 18 | } 19 | 20 | // IsNotFound asserts notFoundError. 21 | func IsNotFound(err error) bool { 22 | return microerror.Cause(err) == notFoundError 23 | } 24 | 25 | var invalidConfigError = µerror.Error{ 26 | Kind: "invalidConfigError", 27 | } 28 | 29 | // IsInvalidConfig asserts invalidConfigError. 30 | func IsInvalidConfig(err error) bool { 31 | return microerror.Cause(err) == invalidConfigError 32 | } 33 | 34 | var wrongTypeError = µerror.Error{ 35 | Kind: "wrongTypeError", 36 | } 37 | 38 | // IsWrongTypeError asserts wrongTypeError. 39 | func IsWrongTypeError(err error) bool { 40 | return microerror.Cause(err) == wrongTypeError 41 | } 42 | -------------------------------------------------------------------------------- /service/controller/resources/vaultcrt/metric.go: -------------------------------------------------------------------------------- 1 | package vaultcrt 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | ) 6 | 7 | const ( 8 | PrometheusNamespace = "cert_operator" 9 | PrometheusSubsystem = "vaultcrt_resource" 10 | ) 11 | 12 | var versionGauge = prometheus.NewGaugeVec( 13 | prometheus.GaugeOpts{ 14 | Namespace: PrometheusNamespace, 15 | Subsystem: PrometheusSubsystem, 16 | Name: "version_total", 17 | Help: "A metric labeled by major, minor and patch version of the operator in use.", 18 | }, 19 | []string{"major", "minor", "patch"}, 20 | ) 21 | 22 | func init() { 23 | prometheus.MustRegister(versionGauge) 24 | } 25 | -------------------------------------------------------------------------------- /service/controller/resources/vaultcrt/resource.go: -------------------------------------------------------------------------------- 1 | package vaultcrt 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/giantswarm/apiextensions/v6/pkg/apis/core/v1alpha1" 7 | "github.com/giantswarm/microerror" 8 | "github.com/giantswarm/micrologger" 9 | "github.com/giantswarm/vaultcrt" 10 | apiv1 "k8s.io/api/core/v1" 11 | "k8s.io/client-go/kubernetes" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | 14 | "github.com/giantswarm/cert-operator/v3/service/controller/key" 15 | ) 16 | 17 | const ( 18 | // AllowSubDomains defines whether to allow the generated root CA of the PKI 19 | // backend to allow sub domains as common names. 20 | AllowSubDomains = true 21 | Name = "vaultcrt" 22 | // ConfigHashAnnotation is the annotation key used to track the hash 23 | // representation of the cert config. This is used to identify changes of the 24 | // config to trigger renewals. 25 | ConfigHashAnnotation = "cert.giantswarm.io/config-hash" 26 | // UpdateTimestampAnnotation is the annotation key used to track the last 27 | // update timestamp of certificates contained in the Kubernetes secrets. 28 | UpdateTimestampAnnotation = "giantswarm.io/update-timestamp" 29 | // UpdateTimestampLayout is the time layout used to format and parse the 30 | // update timestamps tracked in the annotations of the Kubernetes secrets. 31 | UpdateTimestampLayout = "2006-01-02T15:04:05.000000Z" 32 | ) 33 | 34 | type Config struct { 35 | CurrentTimeFactory func() time.Time 36 | CtrlClient client.Client 37 | K8sClient kubernetes.Interface 38 | Logger micrologger.Logger 39 | VaultCrt vaultcrt.Interface 40 | 41 | ExpirationThreshold time.Duration 42 | Namespace string 43 | } 44 | 45 | func DefaultConfig() Config { 46 | return Config{ 47 | CurrentTimeFactory: nil, 48 | CtrlClient: nil, 49 | K8sClient: nil, 50 | Logger: nil, 51 | VaultCrt: nil, 52 | 53 | ExpirationThreshold: 0, 54 | Namespace: "", 55 | } 56 | } 57 | 58 | type Resource struct { 59 | currentTimeFactory func() time.Time 60 | ctrlClient client.Client 61 | k8sClient kubernetes.Interface 62 | logger micrologger.Logger 63 | vaultCrt vaultcrt.Interface 64 | 65 | expirationThreshold time.Duration 66 | namespace string 67 | } 68 | 69 | func New(config Config) (*Resource, error) { 70 | if config.K8sClient == nil { 71 | return nil, microerror.Maskf(invalidConfigError, "config.K8sClient must not be empty") 72 | } 73 | if config.CtrlClient == nil { 74 | return nil, microerror.Maskf(invalidConfigError, "config.CtrlClient must not be empty") 75 | } 76 | if config.CurrentTimeFactory == nil { 77 | return nil, microerror.Maskf(invalidConfigError, "config.CurrentTimeFactory must not be empty") 78 | } 79 | if config.Logger == nil { 80 | return nil, microerror.Maskf(invalidConfigError, "config.Logger must not be empty") 81 | } 82 | if config.VaultCrt == nil { 83 | return nil, microerror.Maskf(invalidConfigError, "config.VaultCrt must not be empty") 84 | } 85 | 86 | if config.ExpirationThreshold == 0 { 87 | return nil, microerror.Maskf(invalidConfigError, "config.ExpirationThreshold must not be empty") 88 | } 89 | if config.Namespace == "" { 90 | return nil, microerror.Maskf(invalidConfigError, "config.Namespace must not be empty") 91 | } 92 | 93 | r := &Resource{ 94 | currentTimeFactory: config.CurrentTimeFactory, 95 | ctrlClient: config.CtrlClient, 96 | k8sClient: config.K8sClient, 97 | logger: config.Logger.With( 98 | "resource", Name, 99 | ), 100 | vaultCrt: config.VaultCrt, 101 | 102 | expirationThreshold: config.ExpirationThreshold, 103 | namespace: config.Namespace, 104 | } 105 | 106 | return r, nil 107 | } 108 | 109 | func (r *Resource) Name() string { 110 | return Name 111 | } 112 | 113 | func (r *Resource) issueCertificate(customObject v1alpha1.CertConfig) (string, string, string, error) { 114 | c := vaultcrt.CreateConfig{ 115 | AltNames: key.AltNames(customObject), 116 | CommonName: key.CommonName(customObject), 117 | ID: key.ClusterID(customObject), 118 | IPSANs: key.IPSANs(customObject), 119 | Organizations: key.Organizations(customObject), 120 | TTL: key.CrtTTL(customObject), 121 | } 122 | result, err := r.vaultCrt.Create(c) 123 | if err != nil { 124 | return "", "", "", microerror.Mask(err) 125 | } 126 | 127 | return result.CA, result.Crt, result.Key, nil 128 | } 129 | 130 | func toSecret(v interface{}) (*apiv1.Secret, error) { 131 | if v == nil { 132 | return nil, nil 133 | } 134 | 135 | secret, ok := v.(*apiv1.Secret) 136 | if !ok { 137 | return nil, microerror.Maskf(wrongTypeError, "expected '%T', got '%T'", &apiv1.Secret{}, v) 138 | } 139 | 140 | return secret, nil 141 | } 142 | -------------------------------------------------------------------------------- /service/controller/resources/vaultcrt/update.go: -------------------------------------------------------------------------------- 1 | package vaultcrt 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/giantswarm/apiextensions/v6/pkg/apis/core/v1alpha1" 8 | "github.com/giantswarm/certs/v4/pkg/certs" 9 | "github.com/giantswarm/microerror" 10 | "github.com/giantswarm/operatorkit/v7/pkg/resource/crud" 11 | apiv1 "k8s.io/api/core/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | 14 | "github.com/giantswarm/cert-operator/v3/service/controller/key" 15 | ) 16 | 17 | func (r *Resource) ApplyUpdateChange(ctx context.Context, obj, updateChange interface{}) error { 18 | secretToUpdate, err := toSecret(updateChange) 19 | if err != nil { 20 | return microerror.Mask(err) 21 | } 22 | 23 | if secretToUpdate != nil { 24 | customObject, err := key.ToCustomObject(obj) 25 | if err != nil { 26 | return microerror.Mask(err) 27 | } 28 | r.logger.LogCtx(ctx, "level", "debug", "message", "updating the secret in the Kubernetes API") 29 | 30 | _, err = r.k8sClient.CoreV1().Secrets(customObject.GetNamespace()).Update(ctx, secretToUpdate, metav1.UpdateOptions{}) 31 | if err != nil { 32 | return microerror.Mask(err) 33 | } 34 | 35 | r.logger.LogCtx(ctx, "level", "debug", "message", "updated the secret in the Kubernetes API") 36 | } else { 37 | r.logger.LogCtx(ctx, "level", "debug", "message", "the secret does not need to be updated in the Kubernetes API") 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func (r *Resource) NewUpdatePatch(ctx context.Context, obj, currentState, desiredState interface{}) (*crud.Patch, error) { 44 | create, err := r.newCreateChange(ctx, obj, currentState, desiredState) 45 | if err != nil { 46 | return nil, microerror.Mask(err) 47 | } 48 | 49 | update, err := r.newUpdateChange(ctx, obj, currentState, desiredState) 50 | if err != nil { 51 | return nil, microerror.Mask(err) 52 | } 53 | 54 | patch := crud.NewPatch() 55 | patch.SetCreateChange(create) 56 | patch.SetUpdateChange(update) 57 | 58 | return patch, nil 59 | } 60 | 61 | func (r *Resource) newUpdateChange(ctx context.Context, obj, currentState, desiredState interface{}) (interface{}, error) { 62 | customObject, err := key.ToCustomObject(obj) 63 | if err != nil { 64 | return nil, microerror.Mask(err) 65 | } 66 | currentSecret, err := toSecret(currentState) 67 | if err != nil { 68 | return nil, microerror.Mask(err) 69 | } 70 | desiredSecret, err := toSecret(desiredState) 71 | if err != nil { 72 | return nil, microerror.Mask(err) 73 | } 74 | 75 | r.logger.LogCtx(ctx, "level", "debug", "message", "finding out if the secret has to be updated") 76 | 77 | var secretToUpdate *apiv1.Secret 78 | { 79 | TTL, err := time.ParseDuration(key.CrtTTL(customObject)) 80 | if err != nil { 81 | return false, microerror.Mask(err) 82 | } 83 | 84 | renew, err := r.shouldCertBeRenewed(customObject, currentSecret, desiredSecret, TTL, r.expirationThreshold) 85 | if IsMissingAnnotation(err) { 86 | // fall through 87 | } else if err != nil { 88 | return nil, microerror.Mask(err) 89 | } 90 | 91 | if renew { 92 | ca, crt, k, err := r.issueCertificate(customObject) 93 | if err != nil { 94 | return nil, microerror.Mask(err) 95 | } 96 | 97 | secretToUpdate = desiredSecret 98 | secretToUpdate.StringData[key.CAID] = ca 99 | secretToUpdate.StringData[key.CrtID] = crt 100 | secretToUpdate.StringData[key.KeyID] = k 101 | } 102 | } 103 | 104 | r.logger.LogCtx(ctx, "level", "debug", "message", "found out if the secret has to be updated") 105 | 106 | return secretToUpdate, nil 107 | } 108 | 109 | func (r *Resource) shouldCertBeRenewed(customObject v1alpha1.CertConfig, currentSecret, desiredSecret *apiv1.Secret, TTL, threshold time.Duration) (bool, error) { 110 | // Check if there are annotations at all. 111 | { 112 | if currentSecret == nil { 113 | return false, microerror.Maskf(missingAnnotationError, "current secret") 114 | } 115 | if currentSecret.Annotations == nil { 116 | return false, microerror.Maskf(missingAnnotationError, "current secret") 117 | } 118 | if desiredSecret == nil { 119 | return false, microerror.Maskf(missingAnnotationError, "desired secret") 120 | } 121 | if desiredSecret.Annotations == nil { 122 | return false, microerror.Maskf(missingAnnotationError, "desired secret") 123 | } 124 | } 125 | 126 | // Check if the cert configs ask to disable regeneration. 127 | { 128 | // TODO remove this hack once all cert configs are updated with the correct 129 | // value for DisableRegeneration. 130 | if customObject.Spec.Cert.ClusterComponent == string(certs.ServiceAccountCert) { 131 | return false, nil 132 | } 133 | if customObject.Spec.Cert.DisableRegeneration { 134 | return false, nil 135 | } 136 | } 137 | 138 | // Check the update timestamp annotation. 139 | { 140 | a, ok := currentSecret.Annotations[UpdateTimestampAnnotation] 141 | if !ok { 142 | return false, microerror.Maskf(missingAnnotationError, "current secret") 143 | } 144 | 145 | t, err := time.ParseInLocation(UpdateTimestampLayout, a, time.UTC) 146 | if err != nil { 147 | return false, microerror.Mask(err) 148 | } 149 | 150 | if t.Add(TTL).Add(-threshold).Before(r.currentTimeFactory()) { 151 | return true, nil 152 | } 153 | } 154 | 155 | // Check the config hash annotation. 156 | { 157 | c, ok := currentSecret.Annotations[ConfigHashAnnotation] 158 | if !ok { 159 | return true, nil 160 | } 161 | d, ok := desiredSecret.Annotations[ConfigHashAnnotation] 162 | if !ok { 163 | return false, microerror.Maskf(missingAnnotationError, "desired secret") 164 | } 165 | 166 | if c != d { 167 | return true, nil 168 | } 169 | } 170 | 171 | return false, nil 172 | } 173 | -------------------------------------------------------------------------------- /service/controller/resources/vaultpki/create.go: -------------------------------------------------------------------------------- 1 | package vaultpki 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/giantswarm/microerror" 7 | 8 | "github.com/giantswarm/cert-operator/v3/service/controller/key" 9 | ) 10 | 11 | func (r *Resource) ApplyCreateChange(ctx context.Context, obj, createChange interface{}) error { 12 | customObject, err := key.ToCustomObject(obj) 13 | if err != nil { 14 | return microerror.Mask(err) 15 | } 16 | vaultPKIStateToCreate, err := toVaultPKIState(createChange) 17 | if err != nil { 18 | return microerror.Mask(err) 19 | } 20 | 21 | if vaultPKIStateToCreate.Backend != nil { 22 | r.logger.LogCtx(ctx, "level", "debug", "message", "creating the Vault PKI in the Vault API") 23 | 24 | err := r.vaultPKI.CreateBackend(key.ClusterID(customObject)) 25 | if err != nil { 26 | return microerror.Mask(err) 27 | } 28 | 29 | r.logger.LogCtx(ctx, "level", "debug", "message", "created the Vault PKI in the Vault API") 30 | } else { 31 | r.logger.LogCtx(ctx, "level", "debug", "message", "the Vault PKI does not need to be created in the Vault API") 32 | } 33 | 34 | if vaultPKIStateToCreate.CACertificate != "" { 35 | r.logger.LogCtx(ctx, "level", "debug", "message", "creating the root CA in the Vault PKI") 36 | 37 | _, err := r.vaultPKI.CreateCA(key.ClusterID(customObject)) 38 | if err != nil { 39 | return microerror.Mask(err) 40 | } 41 | 42 | r.logger.LogCtx(ctx, "level", "debug", "message", "created the root CA in the Vault PKI") 43 | } else { 44 | r.logger.LogCtx(ctx, "level", "debug", "message", "the root CA does not need to be created in the Vault PKI") 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func (r *Resource) newCreateChange(ctx context.Context, obj, currentState, desiredState interface{}) (interface{}, error) { 51 | currentVaultPKIState, err := toVaultPKIState(currentState) 52 | if err != nil { 53 | return nil, microerror.Mask(err) 54 | } 55 | desiredVaultPKIState, err := toVaultPKIState(desiredState) 56 | if err != nil { 57 | return nil, microerror.Mask(err) 58 | } 59 | 60 | r.logger.LogCtx(ctx, "level", "debug", "message", "finding out if the Vault PKI has to be created") 61 | 62 | var vaultPKIStateToCreate VaultPKIState 63 | if currentVaultPKIState.Backend == nil { 64 | vaultPKIStateToCreate.Backend = desiredVaultPKIState.Backend 65 | } 66 | if currentVaultPKIState.CACertificate == "" { 67 | vaultPKIStateToCreate.CACertificate = desiredVaultPKIState.CACertificate 68 | } 69 | 70 | r.logger.LogCtx(ctx, "level", "debug", "message", "found out if the Vault PKI has to be created") 71 | 72 | return vaultPKIStateToCreate, nil 73 | } 74 | -------------------------------------------------------------------------------- /service/controller/resources/vaultpki/create_test.go: -------------------------------------------------------------------------------- 1 | package vaultpki 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/giantswarm/apiextensions/v6/pkg/apis/core/v1alpha1" 9 | "github.com/giantswarm/micrologger/microloggertest" 10 | "github.com/giantswarm/vaultpki/vaultpkitest" 11 | vaultapi "github.com/hashicorp/vault/api" 12 | ) 13 | 14 | func Test_Resource_VaultPKI_NewCreateChange(t *testing.T) { 15 | testCases := []struct { 16 | Obj interface{} 17 | CurrentState interface{} 18 | DesiredState interface{} 19 | ExpectedState VaultPKIState 20 | }{ 21 | // Test 0 ensures that zero value input results in zero value output. 22 | { 23 | Obj: &v1alpha1.CertConfig{ 24 | Spec: v1alpha1.CertConfigSpec{ 25 | Cert: v1alpha1.CertConfigSpecCert{ 26 | ClusterID: "foobar", 27 | }, 28 | }, 29 | }, 30 | CurrentState: VaultPKIState{}, 31 | DesiredState: VaultPKIState{}, 32 | ExpectedState: VaultPKIState{}, 33 | }, 34 | 35 | // Test 1 ensures that the current state is reversed using the desired 36 | // state. In case the backend state is nil and the CA certificate state is 37 | // not empty within the current state, the create state should contain the 38 | // backend state from the desired state and the CA certificate state should 39 | // be empty. 40 | { 41 | Obj: &v1alpha1.CertConfig{ 42 | Spec: v1alpha1.CertConfigSpec{ 43 | Cert: v1alpha1.CertConfigSpecCert{ 44 | ClusterID: "foobar", 45 | }, 46 | }, 47 | }, 48 | CurrentState: VaultPKIState{ 49 | Backend: nil, 50 | CACertificate: "placeholder", 51 | }, 52 | DesiredState: VaultPKIState{ 53 | Backend: &vaultapi.MountOutput{ 54 | Type: "pki", 55 | }, 56 | CACertificate: "placeholder", 57 | }, 58 | ExpectedState: VaultPKIState{ 59 | Backend: &vaultapi.MountOutput{ 60 | Type: "pki", 61 | }, 62 | CACertificate: "", 63 | }, 64 | }, 65 | 66 | // Test 2 ensures that the current state is reversed using the desired 67 | // state. In case the backend state is not nil and the CA certificate state 68 | // is empty within the current state, the create state should contain a nil 69 | // backend state and the CA certificate state should be defined by the 70 | // desired state. 71 | { 72 | Obj: &v1alpha1.CertConfig{ 73 | Spec: v1alpha1.CertConfigSpec{ 74 | Cert: v1alpha1.CertConfigSpecCert{ 75 | ClusterID: "foobar", 76 | }, 77 | }, 78 | }, 79 | CurrentState: VaultPKIState{ 80 | Backend: &vaultapi.MountOutput{ 81 | Type: "pki", 82 | }, 83 | CACertificate: "", 84 | }, 85 | DesiredState: VaultPKIState{ 86 | Backend: &vaultapi.MountOutput{ 87 | Type: "pki", 88 | }, 89 | CACertificate: "placeholder", 90 | }, 91 | ExpectedState: VaultPKIState{ 92 | Backend: nil, 93 | CACertificate: "placeholder", 94 | }, 95 | }, 96 | 97 | // Test 3 ensures that a complete current state results in a completely 98 | // empty create state. 99 | { 100 | Obj: &v1alpha1.CertConfig{ 101 | Spec: v1alpha1.CertConfigSpec{ 102 | Cert: v1alpha1.CertConfigSpecCert{ 103 | ClusterID: "foobar", 104 | }, 105 | }, 106 | }, 107 | CurrentState: VaultPKIState{ 108 | Backend: &vaultapi.MountOutput{ 109 | Type: "pki", 110 | }, 111 | CACertificate: "placeholder", 112 | }, 113 | DesiredState: VaultPKIState{ 114 | Backend: &vaultapi.MountOutput{ 115 | Type: "pki", 116 | }, 117 | CACertificate: "placeholder", 118 | }, 119 | ExpectedState: VaultPKIState{ 120 | Backend: nil, 121 | CACertificate: "", 122 | }, 123 | }, 124 | } 125 | 126 | var err error 127 | var newResource *Resource 128 | { 129 | c := Config{ 130 | Logger: microloggertest.New(), 131 | VaultPKI: vaultpkitest.New(), 132 | } 133 | 134 | newResource, err = New(c) 135 | if err != nil { 136 | t.Fatal("expected", nil, "got", err) 137 | } 138 | } 139 | 140 | for i, tc := range testCases { 141 | result, err := newResource.newCreateChange(context.TODO(), tc.Obj, tc.CurrentState, tc.DesiredState) 142 | if err != nil { 143 | t.Fatal("case", i, "expected", nil, "got", err) 144 | } 145 | r := result.(VaultPKIState) 146 | if !reflect.DeepEqual(r, tc.ExpectedState) { 147 | t.Fatalf("case %d expected %#v got %#v", i, tc.ExpectedState, r) 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /service/controller/resources/vaultpki/current.go: -------------------------------------------------------------------------------- 1 | package vaultpki 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/giantswarm/microerror" 7 | "github.com/giantswarm/vaultpki" 8 | 9 | "github.com/giantswarm/cert-operator/v3/service/controller/key" 10 | ) 11 | 12 | func (r *Resource) GetCurrentState(ctx context.Context, obj interface{}) (interface{}, error) { 13 | customObject, err := key.ToCustomObject(obj) 14 | if err != nil { 15 | return nil, microerror.Mask(err) 16 | } 17 | 18 | var vaultPKIState VaultPKIState 19 | 20 | { 21 | r.logger.LogCtx(ctx, "level", "debug", "message", "looking for the Vault PKI in the Vault API") 22 | 23 | backend, err := r.vaultPKI.GetBackend(key.ClusterID(customObject)) 24 | if vaultpki.IsNotFound(err) { 25 | r.logger.LogCtx(ctx, "level", "debug", "message", "did not find the Vault PKI in the Vault API") 26 | } else if err != nil { 27 | return false, microerror.Mask(err) 28 | } else { 29 | r.logger.LogCtx(ctx, "level", "debug", "message", "found the Vault PKI in the Vault API") 30 | 31 | vaultPKIState.Backend = backend 32 | } 33 | } 34 | 35 | { 36 | r.logger.LogCtx(ctx, "level", "debug", "message", "looking for the root CA in the Vault PKI") 37 | 38 | caCertificate, err := r.vaultPKI.GetCACertificate(key.ClusterID(customObject)) 39 | if vaultpki.IsNotFound(err) { 40 | r.logger.LogCtx(ctx, "level", "debug", "message", "did not find the root CA in the Vault PKI") 41 | } else if err != nil { 42 | return false, microerror.Mask(err) 43 | } else { 44 | r.logger.LogCtx(ctx, "level", "debug", "message", "found the root CA in the Vault PKI") 45 | 46 | vaultPKIState.CACertificate = caCertificate.Certificate 47 | } 48 | } 49 | 50 | return vaultPKIState, nil 51 | } 52 | -------------------------------------------------------------------------------- /service/controller/resources/vaultpki/delete.go: -------------------------------------------------------------------------------- 1 | package vaultpki 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/giantswarm/microerror" 7 | "github.com/giantswarm/operatorkit/v7/pkg/resource/crud" 8 | 9 | "github.com/giantswarm/cert-operator/v3/service/controller/key" 10 | ) 11 | 12 | func (r *Resource) NewDeletePatch(ctx context.Context, obj, currentState, desiredState interface{}) (*crud.Patch, error) { 13 | delete, err := r.newDeleteChange(ctx, obj, currentState, desiredState) 14 | if err != nil { 15 | return nil, microerror.Mask(err) 16 | } 17 | 18 | patch := crud.NewPatch() 19 | patch.SetDeleteChange(delete) 20 | 21 | return patch, nil 22 | } 23 | 24 | func (r *Resource) ApplyDeleteChange(ctx context.Context, obj, deleteChange interface{}) error { 25 | customObject, err := key.ToCustomObject(obj) 26 | if err != nil { 27 | return microerror.Mask(err) 28 | } 29 | vaultPKIStateToDelete, err := toVaultPKIState(deleteChange) 30 | if err != nil { 31 | return microerror.Mask(err) 32 | } 33 | 34 | if vaultPKIStateToDelete.Backend != nil || vaultPKIStateToDelete.CACertificate != "" { 35 | r.logger.LogCtx(ctx, "level", "debug", "message", "deleting the Vault PKI in the Vault API") 36 | 37 | err := r.vaultPKI.DeleteBackend(key.ClusterID(customObject)) 38 | if err != nil { 39 | return microerror.Mask(err) 40 | } 41 | 42 | r.logger.LogCtx(ctx, "level", "debug", "message", "deleted the Vault PKI in the Vault API") 43 | } else { 44 | r.logger.LogCtx(ctx, "level", "debug", "message", "the Vault PKI does not need to be deleted from the Vault API") 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func (r *Resource) newDeleteChange(ctx context.Context, obj, currentState, desiredState interface{}) (interface{}, error) { 51 | _, err := toVaultPKIState(currentState) 52 | if err != nil { 53 | return nil, microerror.Mask(err) 54 | } 55 | _, err = toVaultPKIState(desiredState) 56 | if err != nil { 57 | return nil, microerror.Mask(err) 58 | } 59 | 60 | // We do not delete tenant cluster PKI when a CertConfig is deleted. 61 | var vaultPKIStateToDelete VaultPKIState 62 | 63 | return vaultPKIStateToDelete, nil 64 | } 65 | -------------------------------------------------------------------------------- /service/controller/resources/vaultpki/delete_test.go: -------------------------------------------------------------------------------- 1 | package vaultpki 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/giantswarm/apiextensions/v6/pkg/apis/core/v1alpha1" 9 | "github.com/giantswarm/micrologger/microloggertest" 10 | "github.com/giantswarm/vaultpki/vaultpkitest" 11 | vaultapi "github.com/hashicorp/vault/api" 12 | ) 13 | 14 | func Test_Resource_VaultPKI_NewDeleteChange(t *testing.T) { 15 | testCases := []struct { 16 | Obj interface{} 17 | CurrentState interface{} 18 | DesiredState interface{} 19 | ExpectedState VaultPKIState 20 | }{ 21 | // Test 0 ensures that zero value input results in zero value output. 22 | { 23 | Obj: &v1alpha1.CertConfig{ 24 | Spec: v1alpha1.CertConfigSpec{ 25 | Cert: v1alpha1.CertConfigSpecCert{ 26 | ClusterID: "foobar", 27 | }, 28 | }, 29 | }, 30 | CurrentState: VaultPKIState{}, 31 | DesiredState: VaultPKIState{}, 32 | ExpectedState: VaultPKIState{}, 33 | }, 34 | 35 | // Test 1 ensures that any input results in zero value output because 36 | // deletion of PKI backends is not allowed. Thus delete state will always be 37 | // empty. 38 | { 39 | Obj: &v1alpha1.CertConfig{ 40 | Spec: v1alpha1.CertConfigSpec{ 41 | Cert: v1alpha1.CertConfigSpecCert{ 42 | ClusterID: "foobar", 43 | }, 44 | }, 45 | }, 46 | CurrentState: VaultPKIState{ 47 | Backend: &vaultapi.MountOutput{ 48 | Type: "pki", 49 | }, 50 | CACertificate: "placeholder", 51 | }, 52 | DesiredState: VaultPKIState{ 53 | Backend: &vaultapi.MountOutput{ 54 | Type: "pki", 55 | }, 56 | CACertificate: "placeholder", 57 | }, 58 | ExpectedState: VaultPKIState{ 59 | Backend: nil, 60 | CACertificate: "", 61 | }, 62 | }, 63 | 64 | // Test 2 is the same as 1 but with different input values. 65 | { 66 | Obj: &v1alpha1.CertConfig{ 67 | Spec: v1alpha1.CertConfigSpec{ 68 | Cert: v1alpha1.CertConfigSpecCert{ 69 | ClusterID: "foobar", 70 | }, 71 | }, 72 | }, 73 | CurrentState: VaultPKIState{ 74 | Backend: nil, 75 | CACertificate: "", 76 | }, 77 | DesiredState: VaultPKIState{ 78 | Backend: &vaultapi.MountOutput{ 79 | Type: "pki", 80 | }, 81 | CACertificate: "placeholder", 82 | }, 83 | ExpectedState: VaultPKIState{ 84 | Backend: nil, 85 | CACertificate: "", 86 | }, 87 | }, 88 | } 89 | 90 | var err error 91 | var newResource *Resource 92 | { 93 | c := Config{ 94 | Logger: microloggertest.New(), 95 | VaultPKI: vaultpkitest.New(), 96 | } 97 | 98 | newResource, err = New(c) 99 | if err != nil { 100 | t.Fatal("expected", nil, "got", err) 101 | } 102 | } 103 | 104 | for i, tc := range testCases { 105 | result, err := newResource.newDeleteChange(context.TODO(), tc.Obj, tc.CurrentState, tc.DesiredState) 106 | if err != nil { 107 | t.Fatal("case", i, "expected", nil, "got", err) 108 | } 109 | r := result.(VaultPKIState) 110 | if !reflect.DeepEqual(r, tc.ExpectedState) { 111 | t.Fatalf("case %d expected %#v got %#v", i, tc.ExpectedState, r) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /service/controller/resources/vaultpki/desired.go: -------------------------------------------------------------------------------- 1 | package vaultpki 2 | 3 | import ( 4 | "context" 5 | 6 | vaultapi "github.com/hashicorp/vault/api" 7 | ) 8 | 9 | func (r *Resource) GetDesiredState(ctx context.Context, obj interface{}) (interface{}, error) { 10 | r.logger.LogCtx(ctx, "level", "debug", "message", "computing the desired Vault PKI") 11 | 12 | // NOTE that we only define a sparse desired state. This is good enough 13 | // because we only need a non-zero-value desired state to do the proper 14 | // reconciliation. It is also that in case of the CA certificate we cannot 15 | // just predict and define it here, because this is the resonsibility of the 16 | // actual issuer backend, e.g. Vault. 17 | var vaultPKIState VaultPKIState 18 | { 19 | vaultPKIState.Backend = &vaultapi.MountOutput{ 20 | Type: "pki", 21 | } 22 | 23 | vaultPKIState.CACertificate = "placeholder" 24 | } 25 | 26 | r.logger.LogCtx(ctx, "level", "debug", "message", "computed the desired Vault PKI") 27 | 28 | return vaultPKIState, nil 29 | } 30 | -------------------------------------------------------------------------------- /service/controller/resources/vaultpki/desired_test.go: -------------------------------------------------------------------------------- 1 | package vaultpki 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/giantswarm/apiextensions/v6/pkg/apis/core/v1alpha1" 9 | "github.com/giantswarm/micrologger/microloggertest" 10 | "github.com/giantswarm/vaultpki/vaultpkitest" 11 | vaultapi "github.com/hashicorp/vault/api" 12 | ) 13 | 14 | func Test_Resource_VaultPKI_GetDesiredState(t *testing.T) { 15 | testCases := []struct { 16 | Obj interface{} 17 | ExpectedState VaultPKIState 18 | }{ 19 | // test 0 ensures the desired state is always the same placeholder state. 20 | { 21 | Obj: &v1alpha1.CertConfig{ 22 | Spec: v1alpha1.CertConfigSpec{ 23 | Cert: v1alpha1.CertConfigSpecCert{ 24 | ClusterID: "foobar", 25 | }, 26 | }, 27 | }, 28 | ExpectedState: VaultPKIState{ 29 | Backend: &vaultapi.MountOutput{ 30 | Type: "pki", 31 | }, 32 | CACertificate: "placeholder", 33 | }, 34 | }, 35 | 36 | // test 1 is the same as 0 but with a different custom object. 37 | { 38 | Obj: &v1alpha1.CertConfig{ 39 | Spec: v1alpha1.CertConfigSpec{ 40 | Cert: v1alpha1.CertConfigSpecCert{ 41 | ClusterID: "al9qy", 42 | }, 43 | }, 44 | }, 45 | ExpectedState: VaultPKIState{ 46 | Backend: &vaultapi.MountOutput{ 47 | Type: "pki", 48 | }, 49 | CACertificate: "placeholder", 50 | }, 51 | }, 52 | } 53 | 54 | var err error 55 | var newResource *Resource 56 | { 57 | c := Config{ 58 | Logger: microloggertest.New(), 59 | VaultPKI: vaultpkitest.New(), 60 | } 61 | 62 | newResource, err = New(c) 63 | if err != nil { 64 | t.Fatal("expected", nil, "got", err) 65 | } 66 | } 67 | 68 | for i, tc := range testCases { 69 | result, err := newResource.GetDesiredState(context.TODO(), tc.Obj) 70 | if err != nil { 71 | t.Fatal("case", i, "expected", nil, "got", err) 72 | } 73 | r := result.(VaultPKIState) 74 | if !reflect.DeepEqual(r, tc.ExpectedState) { 75 | t.Fatalf("case %d expected %#v got %#v", i, tc.ExpectedState, r) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /service/controller/resources/vaultpki/error.go: -------------------------------------------------------------------------------- 1 | package vaultpki 2 | 3 | import ( 4 | "github.com/giantswarm/microerror" 5 | ) 6 | 7 | var invalidConfigError = µerror.Error{ 8 | Kind: "invalidConfigError", 9 | } 10 | 11 | // IsInvalidConfig asserts invalidConfigError. 12 | func IsInvalidConfig(err error) bool { 13 | return microerror.Cause(err) == invalidConfigError 14 | } 15 | 16 | var wrongTypeError = µerror.Error{ 17 | Kind: "wrongTypeError", 18 | } 19 | 20 | // IsWrongTypeError asserts wrongTypeError. 21 | func IsWrongTypeError(err error) bool { 22 | return microerror.Cause(err) == wrongTypeError 23 | } 24 | -------------------------------------------------------------------------------- /service/controller/resources/vaultpki/resource.go: -------------------------------------------------------------------------------- 1 | package vaultpki 2 | 3 | import ( 4 | "github.com/giantswarm/microerror" 5 | "github.com/giantswarm/micrologger" 6 | "github.com/giantswarm/vaultpki" 7 | ) 8 | 9 | const ( 10 | Name = "vaultpki" 11 | ) 12 | 13 | type Config struct { 14 | Logger micrologger.Logger 15 | VaultPKI vaultpki.Interface 16 | } 17 | 18 | type Resource struct { 19 | logger micrologger.Logger 20 | vaultPKI vaultpki.Interface 21 | } 22 | 23 | func New(config Config) (*Resource, error) { 24 | if config.Logger == nil { 25 | return nil, microerror.Maskf(invalidConfigError, "%T.Logger must not be empty", config) 26 | } 27 | if config.VaultPKI == nil { 28 | return nil, microerror.Maskf(invalidConfigError, "%T.VaultPKI must not be empty", config) 29 | } 30 | 31 | r := &Resource{ 32 | logger: config.Logger, 33 | vaultPKI: config.VaultPKI, 34 | } 35 | 36 | return r, nil 37 | } 38 | 39 | func (r *Resource) Name() string { 40 | return Name 41 | } 42 | 43 | func toVaultPKIState(v interface{}) (VaultPKIState, error) { 44 | if v == nil { 45 | return VaultPKIState{}, nil 46 | } 47 | 48 | vaultPKIState, ok := v.(VaultPKIState) 49 | if !ok { 50 | return VaultPKIState{}, microerror.Maskf(wrongTypeError, "expected '%T', got '%T'", VaultPKIState{}, v) 51 | } 52 | 53 | return vaultPKIState, nil 54 | } 55 | -------------------------------------------------------------------------------- /service/controller/resources/vaultpki/spec.go: -------------------------------------------------------------------------------- 1 | package vaultpki 2 | 3 | import vaultapi "github.com/hashicorp/vault/api" 4 | 5 | type VaultPKIState struct { 6 | Backend *vaultapi.MountOutput 7 | CACertificate string 8 | } 9 | -------------------------------------------------------------------------------- /service/controller/resources/vaultpki/update.go: -------------------------------------------------------------------------------- 1 | package vaultpki 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/giantswarm/microerror" 7 | "github.com/giantswarm/operatorkit/v7/pkg/resource/crud" 8 | ) 9 | 10 | func (r *Resource) NewUpdatePatch(ctx context.Context, obj, currentState, desiredState interface{}) (*crud.Patch, error) { 11 | create, err := r.newCreateChange(ctx, obj, currentState, desiredState) 12 | if err != nil { 13 | return nil, microerror.Mask(err) 14 | } 15 | 16 | update, err := r.newUpdateChange(ctx, obj, currentState, desiredState) 17 | if err != nil { 18 | return nil, microerror.Mask(err) 19 | } 20 | 21 | patch := crud.NewPatch() 22 | patch.SetCreateChange(create) 23 | patch.SetUpdateChange(update) 24 | 25 | return patch, nil 26 | } 27 | 28 | func (r *Resource) ApplyUpdateChange(ctx context.Context, obj, updateChange interface{}) error { 29 | return nil 30 | } 31 | 32 | func (r *Resource) newUpdateChange(ctx context.Context, obj, currentState, desiredState interface{}) (interface{}, error) { 33 | return nil, nil 34 | } 35 | -------------------------------------------------------------------------------- /service/controller/resources/vaultrole/create.go: -------------------------------------------------------------------------------- 1 | package vaultrole 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/giantswarm/microerror" 7 | "github.com/giantswarm/vaultrole" 8 | ) 9 | 10 | func (r *Resource) ApplyCreateChange(ctx context.Context, obj, createChange interface{}) error { 11 | roleToCreate, err := toRole(createChange) 12 | if err != nil { 13 | return microerror.Mask(err) 14 | } 15 | 16 | if roleToCreate != nil { 17 | r.logger.LogCtx(ctx, "debug", "creating the role in the Vault API") 18 | 19 | c := vaultrole.CreateConfig{ 20 | AllowBareDomains: roleToCreate.AllowBareDomains, 21 | AllowSubdomains: roleToCreate.AllowSubdomains, 22 | AltNames: roleToCreate.AltNames, 23 | ID: roleToCreate.ID, 24 | Organizations: roleToCreate.Organizations, 25 | TTL: roleToCreate.TTL.String(), 26 | } 27 | err = r.vaultRole.Create(c) 28 | if err != nil { 29 | return microerror.Mask(err) 30 | } 31 | 32 | r.logger.LogCtx(ctx, "debug", "created the role in the Vault API") 33 | } else { 34 | r.logger.LogCtx(ctx, "debug", "the role does not need to be created in the Vault API") 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func (r *Resource) newCreateChange(ctx context.Context, obj, currentState, desiredState interface{}) (interface{}, error) { 41 | currentRole, err := toRole(currentState) 42 | if err != nil { 43 | return nil, microerror.Mask(err) 44 | } 45 | desiredRole, err := toRole(desiredState) 46 | if err != nil { 47 | return nil, microerror.Mask(err) 48 | } 49 | 50 | r.logger.LogCtx(ctx, "debug", "finding out if the role has to be created") 51 | 52 | var roleToCreate *vaultrole.Role 53 | if currentRole == nil { 54 | roleToCreate = desiredRole 55 | } 56 | 57 | r.logger.LogCtx(ctx, "debug", "found out if the role has to be created") 58 | 59 | return roleToCreate, nil 60 | } 61 | -------------------------------------------------------------------------------- /service/controller/resources/vaultrole/create_test.go: -------------------------------------------------------------------------------- 1 | package vaultrole 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/giantswarm/apiextensions/v6/pkg/apis/core/v1alpha1" 10 | "github.com/giantswarm/micrologger/microloggertest" 11 | "github.com/giantswarm/vaultrole" 12 | "github.com/giantswarm/vaultrole/vaultroletest" 13 | ) 14 | 15 | func Test_Resource_VaultRole_newCreateChange(t *testing.T) { 16 | testCases := []struct { 17 | Obj interface{} 18 | CurrentState interface{} 19 | DesiredState interface{} 20 | ExpectedRole *vaultrole.Role 21 | }{ 22 | // Case 0 ensures zero value input results in zero value output. 23 | { 24 | Obj: &v1alpha1.CertConfig{ 25 | Spec: v1alpha1.CertConfigSpec{ 26 | Cert: v1alpha1.CertConfigSpecCert{ 27 | ClusterID: "foobar", 28 | }, 29 | }, 30 | }, 31 | CurrentState: nil, 32 | DesiredState: nil, 33 | ExpectedRole: nil, 34 | }, 35 | 36 | // Case 1 ensures a given current state results in a nil output when there 37 | // is a nil desired state. 38 | { 39 | Obj: &v1alpha1.CertConfig{ 40 | Spec: v1alpha1.CertConfigSpec{ 41 | Cert: v1alpha1.CertConfigSpecCert{ 42 | ClusterID: "foobar", 43 | }, 44 | }, 45 | }, 46 | CurrentState: &vaultrole.Role{ 47 | AllowBareDomains: true, 48 | AllowSubdomains: true, 49 | AltNames: []string{ 50 | "kubernetes", 51 | "kubernetes.default", 52 | }, 53 | ID: "al9qy", 54 | Organizations: []string{ 55 | "api", 56 | "system:masters", 57 | }, 58 | TTL: 24 * time.Hour, 59 | }, 60 | DesiredState: &vaultrole.Role{}, 61 | ExpectedRole: nil, 62 | }, 63 | 64 | // Case 2 that the desired state defines the output in case the current 65 | // state is nil. 66 | { 67 | Obj: &v1alpha1.CertConfig{ 68 | Spec: v1alpha1.CertConfigSpec{ 69 | Cert: v1alpha1.CertConfigSpecCert{ 70 | ClusterID: "foobar", 71 | }, 72 | }, 73 | }, 74 | CurrentState: nil, 75 | DesiredState: &vaultrole.Role{ 76 | AllowBareDomains: true, 77 | AllowSubdomains: true, 78 | AltNames: []string{ 79 | "kubernetes", 80 | "kubernetes.default", 81 | }, 82 | ID: "al9qy", 83 | Organizations: []string{ 84 | "api", 85 | "system:masters", 86 | }, 87 | TTL: 24 * time.Hour, 88 | }, 89 | ExpectedRole: &vaultrole.Role{ 90 | AllowBareDomains: true, 91 | AllowSubdomains: true, 92 | AltNames: []string{ 93 | "kubernetes", 94 | "kubernetes.default", 95 | }, 96 | ID: "al9qy", 97 | Organizations: []string{ 98 | "api", 99 | "system:masters", 100 | }, 101 | TTL: 24 * time.Hour, 102 | }, 103 | }, 104 | } 105 | 106 | var err error 107 | var newResource *Resource 108 | { 109 | c := DefaultConfig() 110 | 111 | c.Logger = microloggertest.New() 112 | c.VaultRole = vaultroletest.New() 113 | 114 | newResource, err = New(c) 115 | if err != nil { 116 | t.Fatal("expected", nil, "got", err) 117 | } 118 | } 119 | 120 | for i, tc := range testCases { 121 | result, err := newResource.newCreateChange(context.TODO(), tc.Obj, tc.CurrentState, tc.DesiredState) 122 | if err != nil { 123 | t.Fatal("case", i, "expected", nil, "got", err) 124 | } 125 | role := result.(*vaultrole.Role) 126 | if !reflect.DeepEqual(tc.ExpectedRole, role) { 127 | t.Fatalf("case %d expected %#v got %#v", i, tc.ExpectedRole, role) 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /service/controller/resources/vaultrole/current.go: -------------------------------------------------------------------------------- 1 | package vaultrole 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/giantswarm/microerror" 7 | "github.com/giantswarm/vaultrole" 8 | 9 | "github.com/giantswarm/cert-operator/v3/service/controller/key" 10 | ) 11 | 12 | func (r *Resource) GetCurrentState(ctx context.Context, obj interface{}) (interface{}, error) { 13 | customObject, err := key.ToCustomObject(obj) 14 | if err != nil { 15 | return nil, microerror.Mask(err) 16 | } 17 | 18 | r.logger.LogCtx(ctx, "debug", "looking for the role in the Vault API") 19 | 20 | var role *vaultrole.Role 21 | { 22 | c := vaultrole.SearchConfig{ 23 | ID: key.ClusterID(customObject), 24 | Organizations: key.Organizations(customObject), 25 | } 26 | result, err := r.vaultRole.Search(c) 27 | if vaultrole.IsNotFound(err) { 28 | r.logger.LogCtx(ctx, "debug", "did not find the role in the Vault API") 29 | // fall through 30 | } else if err != nil { 31 | return nil, microerror.Mask(err) 32 | } else { 33 | r.logger.LogCtx(ctx, "debug", "found the role in the Vault API") 34 | role = &result 35 | } 36 | } 37 | 38 | return role, nil 39 | } 40 | -------------------------------------------------------------------------------- /service/controller/resources/vaultrole/delete.go: -------------------------------------------------------------------------------- 1 | package vaultrole 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/giantswarm/microerror" 7 | "github.com/giantswarm/operatorkit/v7/pkg/resource/crud" 8 | ) 9 | 10 | func (r *Resource) ApplyDeleteChange(ctx context.Context, obj, deleteChange interface{}) error { 11 | return nil 12 | } 13 | 14 | func (r *Resource) NewDeletePatch(ctx context.Context, obj, currentState, desiredState interface{}) (*crud.Patch, error) { 15 | delete, err := r.newDeleteChange(ctx, obj, currentState, desiredState) 16 | if err != nil { 17 | return nil, microerror.Mask(err) 18 | } 19 | 20 | patch := crud.NewPatch() 21 | patch.SetDeleteChange(delete) 22 | 23 | return patch, nil 24 | } 25 | 26 | func (r *Resource) newDeleteChange(ctx context.Context, obj, currentState, desiredState interface{}) (interface{}, error) { 27 | return nil, nil 28 | } 29 | -------------------------------------------------------------------------------- /service/controller/resources/vaultrole/desired.go: -------------------------------------------------------------------------------- 1 | package vaultrole 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/giantswarm/microerror" 8 | "github.com/giantswarm/vaultrole" 9 | 10 | "github.com/giantswarm/cert-operator/v3/service/controller/key" 11 | ) 12 | 13 | func (r *Resource) GetDesiredState(ctx context.Context, obj interface{}) (interface{}, error) { 14 | customObject, err := key.ToCustomObject(obj) 15 | if err != nil { 16 | return nil, microerror.Mask(err) 17 | } 18 | 19 | r.logger.LogCtx(ctx, "debug", "computing the desired role") 20 | 21 | TTL, err := time.ParseDuration(key.RoleTTL(customObject)) 22 | if err != nil { 23 | return nil, microerror.Mask(err) 24 | } 25 | 26 | role := &vaultrole.Role{ 27 | AllowBareDomains: key.AllowBareDomains(customObject), 28 | AllowSubdomains: AllowSubdomains, 29 | AltNames: key.AltNames(customObject), 30 | ID: key.ClusterID(customObject), 31 | Organizations: key.Organizations(customObject), 32 | TTL: TTL, 33 | } 34 | 35 | r.logger.LogCtx(ctx, "debug", "computed the desired role") 36 | 37 | return role, nil 38 | } 39 | -------------------------------------------------------------------------------- /service/controller/resources/vaultrole/desired_test.go: -------------------------------------------------------------------------------- 1 | package vaultrole 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/giantswarm/apiextensions/v6/pkg/apis/core/v1alpha1" 10 | "github.com/giantswarm/micrologger/microloggertest" 11 | "github.com/giantswarm/vaultrole" 12 | "github.com/giantswarm/vaultrole/vaultroletest" 13 | ) 14 | 15 | func Test_Resource_VaultRole_GetDesiredState(t *testing.T) { 16 | testCases := []struct { 17 | Obj interface{} 18 | ExpectedRole *vaultrole.Role 19 | }{ 20 | // Case 0 ensures the creation of the desired state of the Vault role. 21 | { 22 | Obj: &v1alpha1.CertConfig{ 23 | Spec: v1alpha1.CertConfigSpec{ 24 | Cert: v1alpha1.CertConfigSpecCert{ 25 | AllowBareDomains: true, 26 | AltNames: []string{ 27 | "kubernetes", 28 | "kubernetes.default", 29 | }, 30 | ClusterComponent: "api", 31 | ClusterID: "al9qy", 32 | Organizations: []string{ 33 | "system:masters", 34 | }, 35 | TTL: "24h", 36 | }, 37 | }, 38 | }, 39 | ExpectedRole: &vaultrole.Role{ 40 | AllowBareDomains: true, 41 | AllowSubdomains: true, 42 | AltNames: []string{ 43 | "kubernetes", 44 | "kubernetes.default", 45 | }, 46 | ID: "al9qy", 47 | Organizations: []string{ 48 | "api", 49 | "system:masters", 50 | }, 51 | TTL: 24 * time.Hour, 52 | }, 53 | }, 54 | 55 | // Case 1 is like 0 but with different inputs. 56 | { 57 | Obj: &v1alpha1.CertConfig{ 58 | Spec: v1alpha1.CertConfigSpec{ 59 | Cert: v1alpha1.CertConfigSpecCert{ 60 | AllowBareDomains: false, 61 | AltNames: []string{ 62 | "kubernetes", 63 | "kubernetes.default", 64 | }, 65 | ClusterComponent: "calico", 66 | ClusterID: "al9qy", 67 | Organizations: nil, 68 | TTL: "8h", 69 | }, 70 | }, 71 | }, 72 | ExpectedRole: &vaultrole.Role{ 73 | AllowBareDomains: false, 74 | AllowSubdomains: true, 75 | AltNames: []string{ 76 | "kubernetes", 77 | "kubernetes.default", 78 | }, 79 | ID: "al9qy", 80 | Organizations: []string{ 81 | "calico", 82 | }, 83 | TTL: 8 * time.Hour, 84 | }, 85 | }, 86 | } 87 | 88 | var err error 89 | var newResource *Resource 90 | { 91 | c := DefaultConfig() 92 | 93 | c.Logger = microloggertest.New() 94 | c.VaultRole = vaultroletest.New() 95 | 96 | newResource, err = New(c) 97 | if err != nil { 98 | t.Fatal("expected", nil, "got", err) 99 | } 100 | } 101 | 102 | for i, tc := range testCases { 103 | result, err := newResource.GetDesiredState(context.TODO(), tc.Obj) 104 | if err != nil { 105 | t.Fatal("case", i, "expected", nil, "got", err) 106 | } 107 | role := result.(*vaultrole.Role) 108 | if !reflect.DeepEqual(tc.ExpectedRole, role) { 109 | t.Fatalf("case %d expected %#v got %#v", i, tc.ExpectedRole, role) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /service/controller/resources/vaultrole/error.go: -------------------------------------------------------------------------------- 1 | package vaultrole 2 | 3 | import ( 4 | "github.com/giantswarm/microerror" 5 | ) 6 | 7 | var invalidConfigError = µerror.Error{ 8 | Kind: "invalidConfigError", 9 | } 10 | 11 | // IsInvalidConfig asserts invalidConfigError. 12 | func IsInvalidConfig(err error) bool { 13 | return microerror.Cause(err) == invalidConfigError 14 | } 15 | 16 | var wrongTypeError = µerror.Error{ 17 | Kind: "wrongTypeError", 18 | } 19 | 20 | // IsWrongTypeError asserts wrongTypeError. 21 | func IsWrongTypeError(err error) bool { 22 | return microerror.Cause(err) == wrongTypeError 23 | } 24 | -------------------------------------------------------------------------------- /service/controller/resources/vaultrole/resource.go: -------------------------------------------------------------------------------- 1 | package vaultrole 2 | 3 | import ( 4 | "github.com/giantswarm/microerror" 5 | "github.com/giantswarm/micrologger" 6 | "github.com/giantswarm/vaultrole" 7 | ) 8 | 9 | const ( 10 | // AllowSubdomains defines whether to allow the generated root CA of the PKI 11 | // backend to allow sub domains as common names. 12 | AllowSubdomains = true 13 | Name = "vaultrole" 14 | ) 15 | 16 | type Config struct { 17 | Logger micrologger.Logger 18 | VaultRole vaultrole.Interface 19 | } 20 | 21 | func DefaultConfig() Config { 22 | return Config{ 23 | Logger: nil, 24 | VaultRole: nil, 25 | } 26 | } 27 | 28 | type Resource struct { 29 | logger micrologger.Logger 30 | vaultRole vaultrole.Interface 31 | } 32 | 33 | func New(config Config) (*Resource, error) { 34 | if config.Logger == nil { 35 | return nil, microerror.Maskf(invalidConfigError, "config.Logger must not be empty") 36 | } 37 | if config.VaultRole == nil { 38 | return nil, microerror.Maskf(invalidConfigError, "config.VaultRole must not be empty") 39 | } 40 | 41 | r := &Resource{ 42 | logger: config.Logger.With( 43 | "resource", Name, 44 | ), 45 | vaultRole: config.VaultRole, 46 | } 47 | 48 | return r, nil 49 | } 50 | 51 | func (r *Resource) Name() string { 52 | return Name 53 | } 54 | 55 | func toRole(v interface{}) (*vaultrole.Role, error) { 56 | if v == nil { 57 | return nil, nil 58 | } 59 | 60 | role, ok := v.(*vaultrole.Role) 61 | if !ok { 62 | return nil, microerror.Maskf(wrongTypeError, "expected '%T', got '%T'", &vaultrole.Role{}, v) 63 | } 64 | 65 | return role, nil 66 | } 67 | -------------------------------------------------------------------------------- /service/controller/resources/vaultrole/update.go: -------------------------------------------------------------------------------- 1 | package vaultrole 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | 7 | "github.com/giantswarm/microerror" 8 | "github.com/giantswarm/operatorkit/v7/pkg/resource/crud" 9 | "github.com/giantswarm/vaultrole" 10 | ) 11 | 12 | func (r *Resource) ApplyUpdateChange(ctx context.Context, obj, updateChange interface{}) error { 13 | roleToUpdate, err := toRole(updateChange) 14 | if err != nil { 15 | return microerror.Mask(err) 16 | } 17 | 18 | if roleToUpdate != nil { 19 | r.logger.LogCtx(ctx, "debug", "updating the role in the Vault API") 20 | 21 | c := vaultrole.UpdateConfig{ 22 | AllowBareDomains: roleToUpdate.AllowBareDomains, 23 | AllowSubdomains: roleToUpdate.AllowSubdomains, 24 | AltNames: roleToUpdate.AltNames, 25 | ID: roleToUpdate.ID, 26 | Organizations: roleToUpdate.Organizations, 27 | TTL: roleToUpdate.TTL.String(), 28 | } 29 | err = r.vaultRole.Update(c) 30 | if err != nil { 31 | return microerror.Mask(err) 32 | } 33 | 34 | r.logger.LogCtx(ctx, "debug", "updated the role in the Vault API") 35 | } else { 36 | r.logger.LogCtx(ctx, "debug", "the role does not need to be updated in the Vault API") 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func (r *Resource) NewUpdatePatch(ctx context.Context, obj, currentState, desiredState interface{}) (*crud.Patch, error) { 43 | create, err := r.newCreateChange(ctx, obj, currentState, desiredState) 44 | if err != nil { 45 | return nil, microerror.Mask(err) 46 | } 47 | 48 | update, err := r.newUpdateChange(ctx, obj, currentState, desiredState) 49 | if err != nil { 50 | return nil, microerror.Mask(err) 51 | } 52 | 53 | patch := crud.NewPatch() 54 | patch.SetCreateChange(create) 55 | patch.SetUpdateChange(update) 56 | 57 | return patch, nil 58 | } 59 | 60 | func (r *Resource) newUpdateChange(ctx context.Context, obj, currentState, desiredState interface{}) (interface{}, error) { 61 | currentRole, err := toRole(currentState) 62 | if err != nil { 63 | return nil, microerror.Mask(err) 64 | } 65 | desiredRole, err := toRole(desiredState) 66 | if err != nil { 67 | return nil, microerror.Mask(err) 68 | } 69 | 70 | r.logger.LogCtx(ctx, "debug", "finding out if the role has to be updated") 71 | 72 | var roleToUpdate *vaultrole.Role 73 | if !reflect.DeepEqual(currentRole, desiredRole) { 74 | roleToUpdate = desiredRole 75 | } 76 | 77 | r.logger.LogCtx(ctx, "debug", "found out if the role has to be updated") 78 | 79 | return roleToUpdate, nil 80 | } 81 | -------------------------------------------------------------------------------- /service/controller/resources/vaultrole/update_test.go: -------------------------------------------------------------------------------- 1 | package vaultrole 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/giantswarm/apiextensions/v6/pkg/apis/core/v1alpha1" 10 | "github.com/giantswarm/micrologger/microloggertest" 11 | "github.com/giantswarm/vaultrole" 12 | "github.com/giantswarm/vaultrole/vaultroletest" 13 | ) 14 | 15 | func Test_Resource_VaultRole_newUpdateChange(t *testing.T) { 16 | testCases := []struct { 17 | Obj interface{} 18 | CurrentState interface{} 19 | DesiredState interface{} 20 | ExpectedRole *vaultrole.Role 21 | }{ 22 | // Case 0 ensures zero value input results in zero value output. 23 | { 24 | Obj: &v1alpha1.CertConfig{ 25 | Spec: v1alpha1.CertConfigSpec{ 26 | Cert: v1alpha1.CertConfigSpecCert{ 27 | ClusterID: "foobar", 28 | }, 29 | }, 30 | }, 31 | CurrentState: nil, 32 | DesiredState: nil, 33 | ExpectedRole: nil, 34 | }, 35 | 36 | // Case 1 ensures the expected role is defined by the given desired state. 37 | { 38 | Obj: &v1alpha1.CertConfig{ 39 | Spec: v1alpha1.CertConfigSpec{ 40 | Cert: v1alpha1.CertConfigSpecCert{ 41 | ClusterID: "foobar", 42 | }, 43 | }, 44 | }, 45 | CurrentState: &vaultrole.Role{ 46 | AllowBareDomains: true, 47 | AllowSubdomains: true, 48 | AltNames: []string{ 49 | "kubernetes", 50 | "kubernetes.default", 51 | }, 52 | ID: "al9qy", 53 | Organizations: []string{ 54 | "api", 55 | "system:masters", 56 | }, 57 | TTL: 24 * time.Hour, 58 | }, 59 | DesiredState: &vaultrole.Role{}, 60 | ExpectedRole: &vaultrole.Role{}, 61 | }, 62 | 63 | // Case 2 is the same as 1 but with different inputs. 64 | { 65 | Obj: &v1alpha1.CertConfig{ 66 | Spec: v1alpha1.CertConfigSpec{ 67 | Cert: v1alpha1.CertConfigSpecCert{ 68 | ClusterID: "foobar", 69 | }, 70 | }, 71 | }, 72 | CurrentState: nil, 73 | DesiredState: &vaultrole.Role{ 74 | AllowBareDomains: true, 75 | AllowSubdomains: true, 76 | AltNames: []string{ 77 | "kubernetes", 78 | "kubernetes.default", 79 | }, 80 | ID: "al9qy", 81 | Organizations: []string{ 82 | "api", 83 | "system:masters", 84 | }, 85 | TTL: 24 * time.Hour, 86 | }, 87 | ExpectedRole: &vaultrole.Role{ 88 | AllowBareDomains: true, 89 | AllowSubdomains: true, 90 | AltNames: []string{ 91 | "kubernetes", 92 | "kubernetes.default", 93 | }, 94 | ID: "al9qy", 95 | Organizations: []string{ 96 | "api", 97 | "system:masters", 98 | }, 99 | TTL: 24 * time.Hour, 100 | }, 101 | }, 102 | 103 | // Case 3 is the same as 1 but with different inputs. 104 | { 105 | Obj: &v1alpha1.CertConfig{ 106 | Spec: v1alpha1.CertConfigSpec{ 107 | Cert: v1alpha1.CertConfigSpecCert{ 108 | ClusterID: "foobar", 109 | }, 110 | }, 111 | }, 112 | CurrentState: &vaultrole.Role{ 113 | AllowBareDomains: true, 114 | AllowSubdomains: true, 115 | AltNames: []string{ 116 | "kubernetes", 117 | "kubernetes.default", 118 | }, 119 | ID: "al9qy", 120 | Organizations: []string{ 121 | "api", 122 | "system:masters", 123 | }, 124 | TTL: 24 * time.Hour, 125 | }, 126 | DesiredState: &vaultrole.Role{ 127 | AllowBareDomains: true, 128 | AllowSubdomains: true, 129 | AltNames: []string{ 130 | "al9qy.master", 131 | "kubernetes", 132 | "kubernetes.default", 133 | }, 134 | ID: "al9qy", 135 | Organizations: []string{ 136 | "api", 137 | "system:masters", 138 | }, 139 | TTL: 24 * time.Hour, 140 | }, 141 | ExpectedRole: &vaultrole.Role{ 142 | AllowBareDomains: true, 143 | AllowSubdomains: true, 144 | AltNames: []string{ 145 | "al9qy.master", 146 | "kubernetes", 147 | "kubernetes.default", 148 | }, 149 | ID: "al9qy", 150 | Organizations: []string{ 151 | "api", 152 | "system:masters", 153 | }, 154 | TTL: 24 * time.Hour, 155 | }, 156 | }, 157 | } 158 | 159 | var err error 160 | var newResource *Resource 161 | { 162 | c := DefaultConfig() 163 | 164 | c.Logger = microloggertest.New() 165 | c.VaultRole = vaultroletest.New() 166 | 167 | newResource, err = New(c) 168 | if err != nil { 169 | t.Fatal("expected", nil, "got", err) 170 | } 171 | } 172 | 173 | for i, tc := range testCases { 174 | result, err := newResource.newUpdateChange(context.TODO(), tc.Obj, tc.CurrentState, tc.DesiredState) 175 | if err != nil { 176 | t.Fatal("case", i, "expected", nil, "got", err) 177 | } 178 | role := result.(*vaultrole.Role) 179 | if !reflect.DeepEqual(tc.ExpectedRole, role) { 180 | t.Fatalf("case %d expected %#v got %#v", i, tc.ExpectedRole, role) 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /service/error.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/giantswarm/microerror" 5 | ) 6 | 7 | var invalidConfigError = µerror.Error{ 8 | Kind: "invalidConfigError", 9 | } 10 | 11 | // IsInvalidConfig asserts invalidConfigError. 12 | func IsInvalidConfig(err error) bool { 13 | return microerror.Cause(err) == invalidConfigError 14 | } 15 | -------------------------------------------------------------------------------- /service/service.go: -------------------------------------------------------------------------------- 1 | // Package service implements business logic to create Kubernetes resources 2 | // against the Kubernetes API. 3 | package service 4 | 5 | import ( 6 | "context" 7 | "sync" 8 | 9 | corev1alpha1 "github.com/giantswarm/apiextensions/v6/pkg/apis/core/v1alpha1" 10 | providerv1alpha1 "github.com/giantswarm/apiextensions/v6/pkg/apis/provider/v1alpha1" 11 | "github.com/giantswarm/k8sclient/v7/pkg/k8sclient" 12 | "github.com/giantswarm/k8sclient/v7/pkg/k8srestconfig" 13 | "github.com/giantswarm/microendpoint/service/version" 14 | "github.com/giantswarm/microerror" 15 | "github.com/giantswarm/micrologger" 16 | vaultapi "github.com/hashicorp/vault/api" 17 | "github.com/spf13/viper" 18 | "k8s.io/client-go/rest" 19 | capi "sigs.k8s.io/cluster-api/api/v1beta1" 20 | 21 | clientvault "github.com/giantswarm/cert-operator/v3/client/vault" 22 | "github.com/giantswarm/cert-operator/v3/flag" 23 | "github.com/giantswarm/cert-operator/v3/pkg/project" 24 | "github.com/giantswarm/cert-operator/v3/service/collector" 25 | "github.com/giantswarm/cert-operator/v3/service/controller" 26 | ) 27 | 28 | type Config struct { 29 | // Dependencies. 30 | Logger micrologger.Logger 31 | 32 | // Settings. 33 | Flag *flag.Flag 34 | Viper *viper.Viper 35 | 36 | Description string 37 | GitCommit string 38 | ProjectName string 39 | Source string 40 | Version string 41 | } 42 | 43 | type Service struct { 44 | Version *version.Service 45 | 46 | bootOnce sync.Once 47 | certController *controller.Cert 48 | operatorCollector *collector.Set 49 | } 50 | 51 | func New(config Config) (*Service, error) { 52 | if config.Flag == nil { 53 | return nil, microerror.Maskf(invalidConfigError, "%T.Flag must not be empty", config) 54 | } 55 | if config.Viper == nil { 56 | return nil, microerror.Maskf(invalidConfigError, "%T.Viper must not be empty", config) 57 | } 58 | 59 | var err error 60 | 61 | var restConfig *rest.Config 62 | { 63 | c := k8srestconfig.Config{ 64 | Logger: config.Logger, 65 | 66 | Address: config.Viper.GetString(config.Flag.Service.Kubernetes.Address), 67 | InCluster: config.Viper.GetBool(config.Flag.Service.Kubernetes.InCluster), 68 | KubeConfig: config.Viper.GetString(config.Flag.Service.Kubernetes.KubeConfig), 69 | TLS: k8srestconfig.ConfigTLS{ 70 | CAFile: config.Viper.GetString(config.Flag.Service.Kubernetes.TLS.CAFile), 71 | CrtFile: config.Viper.GetString(config.Flag.Service.Kubernetes.TLS.CrtFile), 72 | KeyFile: config.Viper.GetString(config.Flag.Service.Kubernetes.TLS.KeyFile), 73 | }, 74 | } 75 | 76 | restConfig, err = k8srestconfig.New(c) 77 | if err != nil { 78 | return nil, microerror.Mask(err) 79 | } 80 | } 81 | 82 | var k8sClient *k8sclient.Clients 83 | { 84 | c := k8sclient.ClientsConfig{ 85 | SchemeBuilder: k8sclient.SchemeBuilder{ 86 | capi.AddToScheme, 87 | corev1alpha1.AddToScheme, 88 | providerv1alpha1.AddToScheme, 89 | }, 90 | Logger: config.Logger, 91 | 92 | RestConfig: restConfig, 93 | } 94 | 95 | k8sClient, err = k8sclient.NewClients(c) 96 | if err != nil { 97 | return nil, microerror.Mask(err) 98 | } 99 | } 100 | 101 | var vaultClient *vaultapi.Client 102 | { 103 | vaultConfig := clientvault.Config{ 104 | Flag: config.Flag, 105 | Viper: config.Viper, 106 | } 107 | 108 | vaultClient, err = clientvault.NewClient(vaultConfig) 109 | if err != nil { 110 | return nil, microerror.Mask(err) 111 | } 112 | } 113 | 114 | var certController *controller.Cert 115 | { 116 | c := controller.CertConfig{ 117 | K8sClient: k8sClient, 118 | Logger: config.Logger, 119 | VaultClient: vaultClient, 120 | 121 | UniqueApp: config.Viper.GetBool(config.Flag.Service.App.Unique), 122 | CATTL: config.Viper.GetString(config.Flag.Service.Vault.Config.PKI.CA.TTL), 123 | CRDLabelSelector: config.Viper.GetString(config.Flag.Service.CRD.LabelSelector), 124 | CommonNameFormat: config.Viper.GetString(config.Flag.Service.Vault.Config.PKI.CommonName.Format), 125 | ExpirationThreshold: config.Viper.GetDuration(config.Flag.Service.Resource.VaultCrt.ExpirationThreshold), 126 | Namespace: config.Viper.GetString(config.Flag.Service.Resource.VaultCrt.Namespace), 127 | ProjectName: config.ProjectName, 128 | } 129 | 130 | certController, err = controller.NewCert(c) 131 | if err != nil { 132 | return nil, microerror.Mask(err) 133 | } 134 | } 135 | 136 | var operatorCollector *collector.Set 137 | { 138 | c := collector.SetConfig{ 139 | Logger: config.Logger, 140 | VaultClient: vaultClient, 141 | } 142 | 143 | operatorCollector, err = collector.NewSet(c) 144 | if err != nil { 145 | return nil, microerror.Mask(err) 146 | } 147 | } 148 | 149 | var versionService *version.Service 150 | { 151 | c := version.Config{ 152 | Description: project.Description(), 153 | GitCommit: project.GitSHA(), 154 | Name: project.Name(), 155 | Source: project.Source(), 156 | Version: project.Version(), 157 | } 158 | 159 | versionService, err = version.New(c) 160 | if err != nil { 161 | return nil, microerror.Mask(err) 162 | } 163 | } 164 | 165 | s := &Service{ 166 | Version: versionService, 167 | 168 | bootOnce: sync.Once{}, 169 | certController: certController, 170 | operatorCollector: operatorCollector, 171 | } 172 | 173 | return s, nil 174 | } 175 | 176 | // nolint: errcheck 177 | func (s *Service) Boot() { 178 | s.bootOnce.Do(func() { 179 | go s.certController.Boot(context.Background()) 180 | go s.operatorCollector.Boot(context.Background()) 181 | }) 182 | } 183 | --------------------------------------------------------------------------------