├── .dockerignore ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── build-and-test.yml │ ├── codeql.yml │ ├── e2e.yml │ ├── golangci-lint.yml │ ├── govulncheck.yml │ ├── grype.yml │ ├── image-publish.yml │ ├── nancy.yml │ ├── snyk.yml │ └── trivy.yml ├── .gitignore ├── .golangci.yml ├── .grype.yaml ├── .nancy-ignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── Tiltfile ├── build └── build.go ├── cmd ├── root.go └── serve.go ├── codecov.yml ├── controller └── certificatesigningrequest │ ├── certificatesigningrequest_test.go │ ├── controller.go │ ├── controller_test.go │ ├── helper.go │ └── helper_test.go ├── deploy ├── README.md ├── base │ ├── clusterrolebindings.yaml │ ├── clusterroles.yaml │ ├── deployments.yaml │ ├── kustomization.yaml │ ├── namespaces.yaml │ ├── rolebindings.yaml │ ├── serviceaccounts.yaml │ └── services.yaml ├── ha-install.yaml ├── ha │ ├── kustomization.yaml │ ├── overlays │ │ └── deployments.yaml │ ├── rolebindings-patch.yaml │ ├── rolebindings.yaml │ └── roles.yaml ├── standalone-install.yaml └── standalone │ ├── kustomization.yaml │ └── rolebindings-patch.yaml ├── e2e ├── README.md ├── features │ ├── certificatesigningrequest.feature │ ├── eventrecorder.feature │ ├── livenessprobe.feature │ ├── metrics.feature │ ├── readinessprobe.feature │ └── shell.feature ├── main_test.go ├── scenario_test.go └── util_test.go ├── go.mod ├── go.sum ├── hack ├── CHANGELOG.tpl.md ├── LICENSE.header ├── chglog-config.yaml ├── e2e-kind-config.yaml ├── generate-manifests.sh ├── kind-with-registry.sh ├── teardown-kind-with-registry.sh └── tilt-config.json ├── logger ├── logger.go └── logger_test.go ├── main.go └── metrics └── metrics.go /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.idea 2 | **/.vscode 3 | Dockerfile 4 | bin/ 5 | coverage.out 6 | kubelet-serving-cert-approver* 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | * eol=lf 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | commit-message: 6 | prefix: "chore" 7 | schedule: 8 | interval: "daily" 9 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: build-and-test 2 | on: 3 | push: 4 | branches: 5 | - "*" 6 | pull_request: 7 | permissions: 8 | contents: read 9 | jobs: 10 | build-and-test: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - name: Check out code into the Go module directory 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | cache: true 21 | go-version: 1.24.3 22 | - name: Check project dependencies 23 | run: | 24 | rm go.sum 25 | go mod tidy 26 | git -c color.ui=always diff --exit-code go.mod go.sum 27 | - name: Test 28 | run: go test -race ./... -v -coverprofile=coverage.out 29 | env: 30 | GOEXPERIMENT: nocoverageredesign 31 | - name: Set up kubectl 32 | uses: azure/setup-kubectl@v4 33 | with: 34 | version: v1.31.0 35 | - name: Generate Install Manifests 36 | run: | 37 | hack/generate-manifests.sh 38 | git -c color.ui=always diff --exit-code deploy/ 39 | - name: Upload Coverage Report 40 | uses: codecov/codecov-action@v5 41 | with: 42 | file: ./coverage.out 43 | flags: unittests 44 | name: codecov-umbrella 45 | env: 46 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: codeql 2 | on: 3 | push: 4 | branches-ignore: 5 | - 'dependabot/**' 6 | pull_request: 7 | schedule: 8 | - cron: '0 12 * * 6' 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | codeql: 14 | permissions: 15 | contents: read 16 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 17 | runs-on: ubuntu-22.04 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Set up Go 22 | uses: actions/setup-go@v5 23 | with: 24 | cache: true 25 | go-version: 1.24.3 26 | - name: Initialize CodeQL 27 | uses: github/codeql-action/init@v3 28 | with: 29 | languages: go 30 | - name: Autobuild 31 | uses: github/codeql-action/autobuild@v3 32 | - name: Perform CodeQL Analysis 33 | uses: github/codeql-action/analyze@v3 34 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: e2e-test 2 | on: 3 | push: 4 | branches: 5 | - "*" 6 | pull_request: 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | e2e-test: 12 | runs-on: ubuntu-22.04 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | arrays: 17 | [ 18 | { 19 | node: "v1.22.17", 20 | digest: "f5b2e5698c6c9d6d0adc419c0deae21a425c07d81bbf3b6a6834042f25d4fba2", 21 | }, 22 | { 23 | node: "v1.23.17", 24 | digest: "14d0a9a892b943866d7e6be119a06871291c517d279aedb816a4b4bc0ec0a5b3", 25 | }, 26 | { 27 | node: "v1.24.17", 28 | digest: "bad10f9b98d54586cba05a7eaa1b61c6b90bfc4ee174fdc43a7b75ca75c95e51", 29 | }, 30 | { 31 | node: "v1.25.16", 32 | digest: "6110314339b3b44d10da7d27881849a87e092124afab5956f2e10ecdb463b025", 33 | }, 34 | { 35 | node: "v1.26.15", 36 | digest: "c79602a44b4056d7e48dc20f7504350f1e87530fe953428b792def00bc1076dd", 37 | }, 38 | { 39 | node: "v1.27.16", 40 | digest: "2d21a61643eafc439905e18705b8186f3296384750a835ad7a005dceb9546d20", 41 | }, 42 | { 43 | node: "v1.28.15", 44 | digest: "a7c05c7ae043a0b8c818f5a06188bc2c4098f6cb59ca7d1856df00375d839251", 45 | }, 46 | { 47 | node: "v1.29.12", 48 | digest: "62c0672ba99a4afd7396512848d6fc382906b8f33349ae68fb1dbfe549f70dec", 49 | }, 50 | { 51 | node: "v1.30.8", 52 | digest: "17cd608b3971338d9180b00776cb766c50d0a0b6b904ab4ff52fd3fc5c6369bf", 53 | }, 54 | { 55 | node: "v1.31.4", 56 | digest: "2cb39f7295fe7eafee0842b1052a599a4fb0f8bcf3f83d96c7f4864c357c6c30", 57 | }, 58 | { 59 | node: "v1.32.0", 60 | digest: "c48c62eac5da28cdadcf560d1d8616cfa6783b58f0d94cf63ad1bf49600cb027", 61 | }, 62 | ] 63 | install-mode: 64 | - ha 65 | - standalone 66 | steps: 67 | - name: Check out code into the Go module directory 68 | uses: actions/checkout@v4 69 | with: 70 | fetch-depth: 0 71 | - name: Set up kubectl 72 | uses: azure/setup-kubectl@v4 73 | with: 74 | version: ${{ matrix.arrays.node }} 75 | - name: Set up QEMU 76 | uses: docker/setup-qemu-action@v3.6.0 77 | - name: Set up Docker Buildx 78 | uses: docker/setup-buildx-action@v3.10.0 79 | - name: Build Container Image 80 | uses: docker/build-push-action@v6.18.0 81 | with: 82 | context: . 83 | file: ./Dockerfile 84 | load: true 85 | platforms: linux/amd64 86 | provenance: false 87 | push: false 88 | tags: ghcr.io/${{ github.repository }}:ci 89 | - name: Set up KinD 90 | uses: engineerd/setup-kind@v0.6.2 91 | with: 92 | config: ./hack/e2e-kind-config.yaml 93 | image: docker.io/kindest/node@sha256:${{ matrix.arrays.digest }} 94 | skipClusterLogsExport: true 95 | version: "v0.26.0" 96 | - name: Wait for KiND readiness 97 | run: | 98 | kubectl --namespace kube-system wait --for=condition=ready pod -l tier=control-plane --timeout=300s 99 | kubectl get nodes -o wide 100 | - name: Load image on the nodes of the KinD 101 | run: | 102 | kind load docker-image ghcr.io/${{ github.repository }}:ci 103 | - name: Patch Install Manifests 104 | run: | 105 | sed -i -e 's@imagePullPolicy: Always@imagePullPolicy: IfNotPresent@g' ./deploy/${{ matrix.install-mode }}-install.yaml 106 | sed -i -e 's@image: .*@image: ghcr.io/${{ github.repository }}:ci@g' ./deploy/${{ matrix.install-mode }}-install.yaml 107 | - name: Deploy Application 108 | id: deployment 109 | run: | 110 | kubectl apply -f ./deploy/${{ matrix.install-mode }}-install.yaml 111 | count=0 112 | until [[ $(kubectl --namespace kubelet-serving-cert-approver get pod --selector app.kubernetes.io/name=kubelet-serving-cert-approver 2>/dev/null) ]]; do 113 | count=$((count + 1)) 114 | if [[ "${count}" -eq "150" ]]; then 115 | echo 'Wait Timeout exceeded' >&3 116 | return 1 117 | fi 118 | sleep 2 119 | done 120 | kubectl --namespace kubelet-serving-cert-approver wait --for=condition=ready pod --selector app.kubernetes.io/name=kubelet-serving-cert-approver --timeout=300s 121 | - name: Get deployment failure logs 122 | if: ${{ failure() && steps.deployment.outputs.exit_code != 0 }} 123 | run: | 124 | kubectl --namespace kubelet-serving-cert-approver get events 125 | kubectl --namespace kubelet-serving-cert-approver describe deployments kubelet-serving-cert-approver 126 | - name: Set up Go 127 | uses: actions/setup-go@v5 128 | with: 129 | cache: true 130 | go-version: 1.24.3 131 | - name: Test Approved Certificate Signing Requests 132 | run: go test -tags=e2e -v ./e2e 133 | - name: Get Application logs 134 | if: ${{ always() }} 135 | run: | 136 | kubectl --namespace kubelet-serving-cert-approver logs --selector app.kubernetes.io/name=kubelet-serving-cert-approver --prefix 137 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - "*" 6 | pull_request: 7 | permissions: 8 | contents: read 9 | jobs: 10 | golangci: 11 | name: lint 12 | permissions: 13 | contents: read 14 | pull-requests: read 15 | runs-on: ubuntu-22.04 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | cache: true 22 | go-version: 1.24.3 23 | - name: golangci-lint 24 | uses: golangci/golangci-lint-action@v8 25 | with: 26 | version: v2.1.6 27 | -------------------------------------------------------------------------------- /.github/workflows/govulncheck.yml: -------------------------------------------------------------------------------- 1 | name: govulncheck 2 | on: 3 | push: 4 | schedule: 5 | - cron: '0 12 * * 6' 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | govulncheck: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - name: Check out code into the Go module directory 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | cache: true 21 | go-version: 1.24.3 22 | - name: Install govulncheck Vulnerability Scanner 23 | run: go install golang.org/x/vuln/cmd/govulncheck@latest 24 | - name: Run govulncheck Vulnerability Scanner 25 | run: govulncheck -show=verbose ./... 26 | -------------------------------------------------------------------------------- /.github/workflows/grype.yml: -------------------------------------------------------------------------------- 1 | name: grype 2 | on: 3 | push: 4 | branches-ignore: 5 | - 'dependabot/**' 6 | pull_request: 7 | schedule: 8 | - cron: '0 12 * * 6' 9 | permissions: 10 | contents: read 11 | jobs: 12 | grype: 13 | permissions: 14 | contents: read # for docker/build-push-action to read repo content 15 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 16 | runs-on: ubuntu-22.04 17 | steps: 18 | - name: Check out code into the Go module directory 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v3.6.0 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v3.10.0 26 | - name: Build Container Image 27 | uses: docker/build-push-action@v6.18.0 28 | with: 29 | context: . 30 | file: ./Dockerfile 31 | load: true 32 | platforms: linux/amd64 33 | provenance: false 34 | push: false 35 | tags: ghcr.io/${{ github.repository }}:grype 36 | - name: Run Grype Vulnerability Scanner 37 | uses: anchore/scan-action@v6 38 | id: scan 39 | with: 40 | image: ghcr.io/${{ github.repository }}:grype 41 | - name: Upload Grype Scan Results 42 | uses: github/codeql-action/upload-sarif@v3 43 | with: 44 | sarif_file: ${{ steps.scan.outputs.sarif }} 45 | -------------------------------------------------------------------------------- /.github/workflows/image-publish.yml: -------------------------------------------------------------------------------- 1 | name: image-publish 2 | on: 3 | push: 4 | branches: 5 | - "*" 6 | release: 7 | types: 8 | - published 9 | permissions: 10 | contents: read 11 | jobs: 12 | push-to-registry: 13 | runs-on: ubuntu-22.04 14 | permissions: 15 | contents: read 16 | packages: write 17 | id-token: write 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | - name: Docker meta 24 | id: docker_meta 25 | uses: docker/metadata-action@v5.7.0 26 | with: 27 | images: ghcr.io/${{ github.repository }} 28 | flavor: | 29 | latest=false 30 | tags: | 31 | type=ref,event=branch 32 | type=semver,pattern={{version}} 33 | - name: Install Cosign 34 | uses: sigstore/cosign-installer@v3.8.2 35 | - name: Set up QEMU 36 | uses: docker/setup-qemu-action@v3.6.0 37 | - name: Set up Docker Buildx 38 | uses: docker/setup-buildx-action@v3.10.0 39 | - name: Login to Registry 40 | uses: docker/login-action@v3.4.0 41 | with: 42 | registry: ghcr.io 43 | username: ${{ github.repository_owner }} 44 | password: ${{ secrets.GHCR_TOKEN }} 45 | - name: Build and push 46 | id: docker_build 47 | uses: docker/build-push-action@v6.18.0 48 | with: 49 | context: . 50 | file: ./Dockerfile 51 | labels: ${{ steps.docker_meta.outputs.labels }} 52 | platforms: linux/amd64,linux/arm64 53 | provenance: false 54 | push: true 55 | tags: ${{ steps.docker_meta.outputs.tags }} 56 | - name: Sign the container images 57 | run: | 58 | images="" 59 | for tag in ${TAGS}; do 60 | images+="${tag}@${DIGEST} " 61 | done 62 | cosign sign --yes ${images} 63 | env: 64 | TAGS: ${{ steps.docker_meta.outputs.tags }} 65 | COSIGN_EXPERIMENTAL: 1 66 | DIGEST: ${{ steps.docker_build.outputs.digest }} 67 | -------------------------------------------------------------------------------- /.github/workflows/nancy.yml: -------------------------------------------------------------------------------- 1 | name: nancy 2 | on: 3 | push: 4 | branches-ignore: 5 | - 'dependabot/**' 6 | pull_request: 7 | schedule: 8 | - cron: '0 12 * * 6' 9 | permissions: 10 | contents: read 11 | jobs: 12 | nancy: 13 | permissions: 14 | contents: read 15 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 16 | runs-on: ubuntu-22.04 17 | steps: 18 | - name: Check out code into the Go module directory 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - name: Set up Go 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version: 1.24.3 26 | - name: Create dependency list for Nancy 27 | run: go list -json -m all > go.list 28 | - name: Run Nancy Vulnerability Scanner 29 | uses: sonatype-nexus-community/nancy-github-action@v1.0.3 30 | with: 31 | githubToken: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/snyk.yml: -------------------------------------------------------------------------------- 1 | name: snyk 2 | on: 3 | push: 4 | branches-ignore: 5 | - 'dependabot/**' 6 | schedule: 7 | - cron: '0 12 * * 6' 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | snyk: 13 | permissions: 14 | contents: read # for actions/checkout to fetch code 15 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 16 | runs-on: ubuntu-22.04 17 | steps: 18 | - name: Check out code into the Go module directory 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - name: Run Snyk Vulnerability Scanner 23 | uses: snyk/actions/golang@master 24 | continue-on-error: true 25 | env: 26 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 27 | GOFLAGS: -buildvcs=false 28 | with: 29 | args: --sarif-file-output=./snyk.sarif 30 | - name: Upload Snyk Scan Results 31 | uses: github/codeql-action/upload-sarif@v3 32 | with: 33 | sarif_file: ./snyk.sarif 34 | -------------------------------------------------------------------------------- /.github/workflows/trivy.yml: -------------------------------------------------------------------------------- 1 | name: trivy 2 | on: 3 | push: 4 | branches-ignore: 5 | - 'dependabot/**' 6 | pull_request: 7 | schedule: 8 | - cron: '0 12 * * 6' 9 | permissions: 10 | contents: read 11 | jobs: 12 | trivy: 13 | permissions: 14 | contents: read 15 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 16 | runs-on: ubuntu-22.04 17 | steps: 18 | - name: Check out code into the Go module directory 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v3.6.0 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v3.10.0 26 | - name: Build Container Image 27 | uses: docker/build-push-action@v6.18.0 28 | with: 29 | context: . 30 | file: ./Dockerfile 31 | load: true 32 | platforms: linux/amd64 33 | provenance: false 34 | push: false 35 | tags: ghcr.io/${{ github.repository }}:trivy 36 | - name: Run Trivy Vulnerability Scanner 37 | uses: aquasecurity/trivy-action@0.30.0 38 | with: 39 | image-ref: ghcr.io/${{ github.repository }}:trivy 40 | format: sarif 41 | output: ./trivy-results.sarif 42 | - name: Upload Trivy Scan Results 43 | uses: github/codeql-action/upload-sarif@v3 44 | with: 45 | sarif_file: ./trivy-results.sarif 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.idea 2 | **/.vscode 3 | bin/ 4 | coverage.out 5 | go.list 6 | kubelet-serving-cert-approver* 7 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | concurrency: 4 3 | go: '1.24' 4 | timeout: 10m 5 | issues-exit-code: 1 6 | tests: true 7 | build-tags: 8 | - e2e 9 | 10 | version: "2" 11 | 12 | output: 13 | formats: 14 | text: 15 | path: stdout 16 | 17 | formatters: 18 | settings: 19 | gci: 20 | sections: 21 | - standard 22 | - default 23 | - prefix(github.com/alex1989hu/kubelet-serving-cert-approver) 24 | 25 | linters: 26 | enable: 27 | - asciicheck 28 | - bodyclose 29 | - copyloopvar 30 | - depguard 31 | - dogsled 32 | - dupl 33 | - errcheck 34 | - errorlint 35 | - errname 36 | - exhaustive 37 | - exptostd 38 | - fatcontext 39 | - gochecknoglobals 40 | - goconst 41 | - gocritic 42 | - gocyclo 43 | - godot 44 | - godox 45 | - err113 46 | - goheader 47 | - goprintffuncname 48 | - gosec 49 | - govet 50 | - ineffassign 51 | - lll 52 | - makezero 53 | - misspell 54 | - mnd 55 | - nakedret 56 | - nestif 57 | - nilerr 58 | - nilnesserr 59 | - nlreturn 60 | - noctx 61 | - nolintlint 62 | - paralleltest 63 | - prealloc 64 | - predeclared 65 | - revive 66 | - staticcheck 67 | - testifylint 68 | - testpackage 69 | - thelper 70 | - tparallel 71 | - unconvert 72 | - unparam 73 | - unused 74 | - usetesting 75 | - whitespace 76 | - wrapcheck 77 | - wsl 78 | disable: 79 | - funlen 80 | - gochecknoinits 81 | - gocognit 82 | - gomodguard 83 | - rowserrcheck 84 | - sqlclosecheck 85 | settings: 86 | copyloopvar: 87 | check-alias: true 88 | depguard: 89 | rules: 90 | ioutil: 91 | deny: 92 | - pkg: "io/ioutil" 93 | desc: ioutil is deprecated in Go 1.16" 94 | dupl: 95 | threshold: 100 96 | errcheck: 97 | check-type-assertions: true 98 | check-blank: true 99 | govet: 100 | enable: 101 | - fieldalignment 102 | - shadow 103 | gocyclo: 104 | min-complexity: 10 105 | goconst: 106 | min-len: 3 107 | min-occurrences: 3 108 | goheader: 109 | template-path: ./hack/LICENSE.header 110 | makezero: 111 | always: true 112 | misspell: 113 | locale: US 114 | nolintlint: 115 | allow-unused: false 116 | allow-no-explanation: [] 117 | require-explanation: false 118 | require-specific: true 119 | lll: 120 | line-length: 120 121 | tab-width: 1 122 | promlinter: 123 | strict: true 124 | revive: 125 | rules: 126 | - name: atomic 127 | - name: bare-return 128 | - name: blank-imports 129 | - name: bool-literal-in-expr 130 | - name: call-to-gc 131 | - name: confusing-naming 132 | - name: constant-logical-expr 133 | - name: context-as-argument 134 | - name: context-keys-type 135 | - name: deep-exit 136 | - name: dot-imports 137 | - name: duplicated-imports 138 | - name: empty-block 139 | - name: error-naming 140 | - name: error-return 141 | - name: error-strings 142 | - name: errorf 143 | - name: exported 144 | - name: flag-parameter 145 | - name: get-return 146 | - name: identical-branches 147 | - name: if-return 148 | - name: increment-decrement 149 | - name: indent-error-flow 150 | - name: modifies-parameter 151 | - name: modifies-value-receiver 152 | - name: package-comments 153 | disabled: true 154 | - name: range 155 | - name: range-val-address 156 | - name: range-val-in-closure 157 | - name: receiver-naming 158 | - name: redefines-builtin-id 159 | - name: string-of-int 160 | - name: struct-tag 161 | - name: superfluous-else 162 | - name: time-naming 163 | - name: unconditional-recursion 164 | - name: unexported-naming 165 | - name: unexported-return 166 | - name: unnecessary-stmt 167 | - name: unreachable-code 168 | - name: unused-parameter 169 | - name: unused-receiver 170 | - name: var-declaration 171 | - name: var-naming 172 | - name: waitgroup-by-value 173 | unused: 174 | exported-fields-are-used: false 175 | unparam: 176 | check-exported: false 177 | usetesting: 178 | os-create-temp: true 179 | os-mkdir-temp: true 180 | os-setenv: true 181 | os-temp-dir: true 182 | os-chdir: true 183 | context-background: true 184 | context-todo: true 185 | nakedret: 186 | max-func-lines: 30 187 | prealloc: 188 | simple: true 189 | range-loops: true 190 | for-loops: true 191 | testifylint: 192 | enable-all: true 193 | thelper: 194 | test: 195 | first: true 196 | name: true 197 | begin: true 198 | 199 | issues: 200 | max-issues-per-linter: 0 201 | max-same-issues: 0 202 | new: false 203 | -------------------------------------------------------------------------------- /.grype.yaml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - vulnerability: CVE-2015-5237 # Skip for indirect dependency google.golang.org/protobuf@v1.28.1 3 | package: 4 | name: google.golang.org/protobuf 5 | version: v1.28.1 6 | type: go-module 7 | - vulnerability: CVE-2021-22570 # Skip for indirect dependency google.golang.org/protobuf@v1.28.1 8 | package: 9 | name: google.golang.org/protobuf 10 | version: v1.28.1 11 | type: go-module 12 | -------------------------------------------------------------------------------- /.nancy-ignore: -------------------------------------------------------------------------------- 1 | # Skip for indirect dependency github.com/golang-jwt/jwt/v4@v4.5.0 2 | CVE-2024-51744 3 | CVE-2025-30204 4 | 5 | # Skip for indirect dependency github.com/hashicorp/consul/api@v1.28.2 6 | CVE-2022-29153 7 | CVE-2024-10086 8 | 9 | # Skip for indirect dependency k8s.io/apiserver@v0.31.0 10 | CVE-2020-8561 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | The Contribution Guideline helps you to understand how to contribute to **Kubelet Serving Certificate Approver**. 4 | 5 | ## Development 6 | 7 | To ease development there is an option to use [Tilt](https://tilt.dev/) and [KinD (Kubernetes in Docker)](https://kind.sigs.k8s.io/). 8 | 9 | Once you already installed all of them, you can start the development. 10 | 11 | Files being used here: 12 | 13 | * [hack/kind-with-registry.sh](hack/kind-with-registry.sh) 14 | * [hack/teardown-kind-with-registry.sh](hack/teardown-kind-with-registry.sh) 15 | * [Tiltfile](Tiltfile) 16 | 17 | ```bash 18 | # Start Kubernetes Cluster with local Image Registry 19 | hack/kind-with-registry.sh 20 | # Start Tilt 21 | tilt up 22 | 23 | # Navigate back to console to remove your Tilt development 24 | tilt down --delete-namespaces 25 | # Stop and remove Kubernetes Cluster with local Image Registry 26 | hack/teardown-kind-with-registry.sh 27 | ``` 28 | 29 | ## Pull Requests 30 | 31 | Use the [GitHub flow](https://guides.github.com/introduction/flow/) as main versioning workflow. 32 | 33 | ## Style Guide 34 | 35 | All pull requests shall adhere to the [Conventional Commits specification](https://conventionalcommits.org/). 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Alex Szakaly 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | FROM golang:1.24.3 AS builder 17 | 18 | # To let GitHub CI driven buildx pass build arguments 19 | ARG TARGETOS 20 | ARG TARGETARCH 21 | 22 | # Set necessary environment variables 23 | ENV CGO_ENABLED=0 \ 24 | GOOS=$TARGETOS \ 25 | GOARCH=$TARGETARCH 26 | 27 | # All these steps will be cached 28 | WORKDIR /build 29 | COPY go.mod . 30 | COPY go.sum . 31 | 32 | # Get dependencies - will also be cached if we won't change mod/sum 33 | RUN go mod download 34 | # Copy the source code as the last step 35 | COPY . . 36 | 37 | # Build the binary 38 | RUN GIT_COMMIT=$(git rev-parse --short=8 HEAD || echo "dev" ) && \ 39 | GIT_BRANCH=$(git rev-parse --symbolic-full-name --abbrev-ref HEAD || echo "dirty" ) && \ 40 | BUILD_TIME=$(date -u) && \ 41 | go build -trimpath -o /app/kubelet-serving-cert-approver \ 42 | -ldflags "-buildid= -w -s \ 43 | -X 'github.com/alex1989hu/kubelet-serving-cert-approver/build.GitBranch=$GIT_BRANCH' \ 44 | -X 'github.com/alex1989hu/kubelet-serving-cert-approver/build.GitCommit=$GIT_COMMIT' \ 45 | -X 'github.com/alex1989hu/kubelet-serving-cert-approver/build.Time=$BUILD_TIME'" && \ 46 | if [ "$GOARCH" = "amd64" ]; then CGO_ENABLED=1 go test -race ./... -v ; else go test ./... -v ; fi; 47 | 48 | # Production image 49 | FROM gcr.io/distroless/static-debian12@sha256:16f75ae7665b13825daffba81f12d6b1a16d0e1217c562fadfce0ba77ca7b891 50 | 51 | COPY --from=builder /app/kubelet-serving-cert-approver /app/kubelet-serving-cert-approver 52 | 53 | WORKDIR /app 54 | 55 | USER 65534:65534 56 | 57 | EXPOSE 8080 9090 58 | 59 | ENTRYPOINT ["/app/kubelet-serving-cert-approver"] 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Alex Szakaly 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubelet Serving Certificate Approver 2 | 3 | ![CI](https://github.com/alex1989hu/kubelet-serving-cert-approver/workflows/build-and-test/badge.svg) 4 | ![e2e-test](https://github.com/alex1989hu/kubelet-serving-cert-approver/workflows/e2e-test/badge.svg) 5 | [![codecov](https://codecov.io/gh/alex1989hu/kubelet-serving-cert-approver/branch/main/graph/badge.svg)](https://codecov.io/gh/alex1989hu/kubelet-serving-cert-approver) 6 | 7 | Kubelet Serving Certificate Approver is a custom approving controller which approves `kubernetes.io/kubelet-serving` Certificate Signing Request that kubelet use to serve TLS endpoints. 8 | 9 | ## Why should I use Kubelet Serving Certificate Approver? 10 | 11 | * You want to securely - in terms of trusted Certificate Authoritity (CA) - reach kubelet endpoint 12 | 13 | * Signed serving certificates are honored as a valid kubelet serving certificate by the API server 14 | 15 | * Don't want to use `--kubelet-insecure-tls` flag during installation of [metrics-server](https://github.com/kubernetes-sigs/metrics-server/) 16 | 17 | ## Do I need to have a commercial certificate? 18 | 19 | No. Every Kubernetes cluster has a Cluster Root Certificate Authority (CA). 20 | 21 | ## How do I use Kubelet Serving Certificate Approver? 22 | 23 | To install into your Kubernetes cluster, please navigate to [deploy](deploy) directory. 24 | 25 | *Note: your Kubernetes cluster must be configured with enabled TLS Bootstrapping and provided `rotate-server-certificates: true` kubelet argument.* 26 | 27 | ## Kubernetes Compatibility Matrix 28 | 29 | For older Kubernetes versions (`v1.19`, `v1.20`, `v1.21`) please see [older releases](https://github.com/alex1989hu/kubelet-serving-cert-approver/releases). 30 | 31 | | Version | Compatible | 32 | | -------------- | ---------- | 33 | | `v1.22` | ✓ | 34 | | `v1.23` | ✓ | 35 | | `v1.24` | ✓ | 36 | | `v1.25` | ✓ | 37 | | `v1.26` | ✓ | 38 | | `v1.27` | ✓ | 39 | | `v1.28` | ✓ | 40 | | `v1.29` | ✓ | 41 | | `v1.30` | ✓ | 42 | | `v1.31` | ✓ | 43 | | `v1.32` | ✓ | 44 | 45 | ## Prometheus Metrics 46 | 47 | You can download Prometheus metrics `/metrics` endpoint. 48 | 49 | ### Custom Metrics 50 | 51 | | Metric | Description | 52 | | -------------------------------------------------------------------------- | -------------------------------------------------- | 53 | | `kubelet_serving_cert_approver_approved_certificate_signing_request_count` | The number of approved Certificate Signing Request | 54 | | `kubelet_serving_cert_approver_invalid_certificate_signing_request_count` | The number of invalid Certificate Signing Request | 55 | 56 | ## Reference 57 | 58 | * Original idea: which is unfortunately not maintained. 59 | * Kubernetes TLS bootstrapping: 60 | * Conformant Rules: 61 | 62 | ## License 63 | 64 | Apache License, Version 2.0, see [LICENSE](LICENSE). 65 | -------------------------------------------------------------------------------- /Tiltfile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Alex Szakaly 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | analytics_settings(False) 17 | 18 | settings = read_json('./hack/tilt-config.json', default={}) 19 | 20 | docker_build(settings.get('default_registry') + '/' + settings.get('image_repository') + '/kubelet-serving-cert-approver', 21 | '.', 22 | dockerfile='Dockerfile', 23 | ignore=[ 24 | './.github/', 25 | './.idea/', 26 | './.vscode/', 27 | './deploy/', 28 | './hack/', 29 | './*.md', 30 | './codecov.yml', 31 | './.gitattributes', 32 | './.gitignore', 33 | './.golangci.yml', 34 | ] 35 | ) 36 | 37 | k8s_yaml('./deploy/ha-install.yaml') 38 | 39 | k8s_resource( 40 | workload='kubelet-serving-cert-approver', 41 | port_forwards=[8080, 9090], 42 | objects=['kubelet-serving-cert-approver:namespace', 43 | 'kubelet-serving-cert-approver:serviceaccount', 44 | 'leader-election\\:kubelet-serving-cert-approver:role', 45 | 'certificates\\:kubelet-serving-cert-approver:clusterrole', 46 | 'events\\:kubelet-serving-cert-approver:clusterrole', 47 | 'events\\:kubelet-serving-cert-approver:rolebinding', 48 | 'leader-election\\:kubelet-serving-cert-approver:rolebinding', 49 | 'kubelet-serving-cert-approver:clusterrolebinding' 50 | ], 51 | new_name='cluster-setup', 52 | ) 53 | -------------------------------------------------------------------------------- /build/build.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Alex Szakaly 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | package build 17 | 18 | //nolint:gochecknoglobals 19 | var ( 20 | // GitBranch overridden during build time. 21 | GitBranch = "dev" 22 | 23 | // GitCommit overridden during build time. 24 | GitCommit = "dirty" 25 | 26 | // Time overridden during build time. 27 | Time = "" 28 | ) 29 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Alex Szakaly 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | package cmd 17 | 18 | import ( 19 | "fmt" 20 | "log" 21 | 22 | "github.com/spf13/cobra" 23 | "github.com/spf13/viper" 24 | 25 | "github.com/alex1989hu/kubelet-serving-cert-approver/build" 26 | ) 27 | 28 | const defaultNamespace = "kubelet-serving-cert-approver" 29 | 30 | //nolint:gochecknoglobals 31 | var ( 32 | // namespace represents where to record Kubernetes Events. 33 | namespace string 34 | 35 | // isDebug represents the debug level of logger. 36 | isDebug bool 37 | 38 | // enableLeaderElection represents highly available (HA) mode. 39 | enableLeaderElection bool 40 | ) 41 | 42 | // rootCmd represents the base command when called without any subcommands. 43 | // 44 | //nolint:gochecknoglobals 45 | var rootCmd = &cobra.Command{ 46 | Use: "kubelet-serving-cert-approver", 47 | Version: fmt.Sprintf("(%s/%s)", build.GitBranch, build.GitCommit), 48 | Short: "", 49 | Long: `Approves kubernetes.io/kubelet-serving Certificate Signing Request 50 | that kubelet use to serve TLS endpoints. 51 | `, 52 | } 53 | 54 | // Execute adds all child commands to the root command and sets flags appropriately. 55 | // This is called by main.main(). It only needs to happen once to the rootCmd. 56 | func Execute() { 57 | if err := rootCmd.Execute(); err != nil { 58 | log.Fatalf("Execution failed: %v", err) //nolint:revive // We must immediately exit, deep-exit. 59 | } 60 | } 61 | 62 | func init() { 63 | cobra.OnInitialize(initConfig) 64 | 65 | rootCmd.PersistentFlags().BoolVarP(&isDebug, "debug", "d", false, 66 | "Enable debug logging") 67 | 68 | rootCmd.PersistentFlags().BoolVar(&enableLeaderElection, "enable-leader-election", false, 69 | "Enable leader election for highly available (HA) mode") 70 | 71 | rootCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", defaultNamespace, 72 | "Configure namespace where to execute") 73 | 74 | if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil { 75 | log.Fatalf("Can not bind flags: %v", err) 76 | } 77 | } 78 | 79 | // initConfig reads environment variables if set. 80 | func initConfig() { 81 | viper.AutomaticEnv() 82 | 83 | namespace = viper.GetString("namespace") 84 | } 85 | -------------------------------------------------------------------------------- /cmd/serve.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Alex Szakaly 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | package cmd 17 | 18 | import ( 19 | "github.com/go-logr/zapr" 20 | "github.com/spf13/cobra" 21 | uberzap "go.uber.org/zap" 22 | certificatesv1 "k8s.io/api/certificates/v1" 23 | "k8s.io/apimachinery/pkg/runtime" 24 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 25 | clientgokubernetes "k8s.io/client-go/kubernetes" 26 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 27 | "k8s.io/klog/v2" 28 | ctrl "sigs.k8s.io/controller-runtime" 29 | ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 30 | "sigs.k8s.io/controller-runtime/pkg/healthz" 31 | ctrllog "sigs.k8s.io/controller-runtime/pkg/log" 32 | ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" 33 | ctrlmetricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 34 | 35 | "github.com/alex1989hu/kubelet-serving-cert-approver/build" 36 | "github.com/alex1989hu/kubelet-serving-cert-approver/controller/certificatesigningrequest" 37 | "github.com/alex1989hu/kubelet-serving-cert-approver/logger" 38 | "github.com/alex1989hu/kubelet-serving-cert-approver/metrics" 39 | ) 40 | 41 | //nolint:gochecknoglobals 42 | var ( 43 | scheme = runtime.NewScheme() 44 | 45 | // serveCmd represents the generate command. 46 | serveCmd = &cobra.Command{ 47 | Use: "serve", 48 | Short: "Starts operator for Kubelet Serving Certificate Approver", 49 | Long: "", 50 | Run: func(_ *cobra.Command, _ []string) { 51 | startServer() 52 | }, 53 | } 54 | ) 55 | 56 | func init() { 57 | rootCmd.AddCommand(serveCmd) 58 | 59 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 60 | } 61 | 62 | func startServer() { 63 | setupLog := *logger.CreateLogger() 64 | 65 | setupLog.Info("Kubelet Serving Certificate Approver has been started", 66 | uberzap.String("GitBranch", build.GitBranch), 67 | uberzap.String("GitCommit", build.GitCommit), 68 | uberzap.String("Time", build.Time), 69 | uberzap.String("namespace", namespace), 70 | uberzap.Bool("ha", enableLeaderElection), 71 | uberzap.Bool("debug", isDebug)) 72 | 73 | // Forward client-go klog calls to zap 74 | klog.SetLogger(zapr.NewLogger(logger.CreateLogger().Named("client-go"))) 75 | 76 | setupLog.Info("Try to talk to Kubernetes API Server, will exit in case of failure") 77 | 78 | pProfBindAddress := "0" 79 | 80 | if isDebug { 81 | pProfBindAddress = ":8081" 82 | 83 | setupLog.Info("pprof will be enabled", uberzap.String("port", pProfBindAddress)) 84 | } 85 | 86 | // Set zap logger to all deferred loggers of controller-runtime to prevent it from complaining 87 | // about log.SetLogger never being called 88 | ctrllog.SetLogger(zapr.NewLogger(logger.CreateLogger().Named("ctrllog"))) 89 | 90 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 91 | Client: ctrlclient.Options{ 92 | Cache: &ctrlclient.CacheOptions{ 93 | DisableFor: []ctrlclient.Object{ 94 | &certificatesv1.CertificateSigningRequest{}, 95 | }, 96 | }, 97 | }, 98 | Scheme: scheme, 99 | Metrics: ctrlmetricsserver.Options{BindAddress: ":9090"}, 100 | HealthProbeBindAddress: ":8080", 101 | LeaderElection: enableLeaderElection, 102 | LeaderElectionNamespace: namespace, 103 | LeaderElectionResourceLock: "leases", 104 | LeaderElectionID: "kubelet-serving-certificate-approver", 105 | PprofBindAddress: pProfBindAddress, 106 | }) 107 | if err != nil { 108 | setupLog.Fatal("Unable to start manager", uberzap.Error(err)) 109 | } 110 | 111 | setupLog.Info("Successfully connected to Kubernetes API Server") 112 | 113 | // Add readiness probe 114 | if errReadyzCheck := mgr.AddReadyzCheck("ready-ping", healthz.Ping); errReadyzCheck != nil { 115 | setupLog.Fatal("Unable to add readiness check", uberzap.Error(errReadyzCheck)) 116 | } 117 | 118 | // Add liveness probe 119 | if errHealthzCheck := mgr.AddHealthzCheck("health-ping", healthz.Ping); errHealthzCheck != nil { 120 | setupLog.Fatal("Unable to add health check", uberzap.Error(errHealthzCheck)) 121 | } 122 | 123 | if err = (&certificatesigningrequest.SigningReconciler{ 124 | Client: mgr.GetClient(), 125 | ClientSet: clientgokubernetes.NewForConfigOrDie(mgr.GetConfig()), 126 | Scheme: mgr.GetScheme(), 127 | EventRecorder: mgr.GetEventRecorderFor("kubelet-serving-cert-aprover"), 128 | Logger: logger.CreateLogger().With(uberzap.String("controller", "certificatesigningrequest")), 129 | }).SetupWithManager(mgr); err != nil { 130 | setupLog.Fatal("Unable to create controller", uberzap.Error(err)) 131 | } 132 | 133 | // Add our Prometheus metrics 134 | ctrlmetrics.Registry.MustRegister(metrics.NumberOfApprovedCertificateRequests) 135 | ctrlmetrics.Registry.MustRegister(metrics.NumberOfInvalidCertificateSigningRequests) 136 | 137 | setupLog.Debug("Certificate Signing Request Reconciler has ben added") 138 | 139 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 140 | setupLog.Fatal("Unable to start manager", uberzap.Error(err)) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: false 3 | 4 | coverage: 5 | status: 6 | project: 7 | default: 8 | target: 89% 9 | threshold: 0.5% 10 | base: auto 11 | if_ci_failed: success 12 | patch: off 13 | 14 | comment: 15 | layout: "condensed_header, condensed_files, condensed_footer" 16 | -------------------------------------------------------------------------------- /controller/certificatesigningrequest/certificatesigningrequest_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Alex Szakaly 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | package certificatesigningrequest //nolint:testpackage // Need to reach functions. 17 | 18 | import ( 19 | "testing" 20 | 21 | "go.uber.org/goleak" 22 | "go.uber.org/zap" 23 | "go.uber.org/zap/zapcore" 24 | ) 25 | 26 | //nolint:gochecknoglobals 27 | var TestLogger *zap.Logger 28 | 29 | func TestMain(m *testing.M) { 30 | // Create logger with enabled debug logging to reveal any possible issue due to zap CheckedEntry logging 31 | TestLogger, _ = zap.Config{ //nolint:errcheck 32 | Level: zap.NewAtomicLevelAt(zap.DebugLevel), 33 | Development: true, 34 | Encoding: "json", 35 | EncoderConfig: zapcore.EncoderConfig{ 36 | TimeKey: "time", 37 | LevelKey: "level", 38 | NameKey: "logger", 39 | CallerKey: "caller", 40 | MessageKey: "msg", 41 | LineEnding: zapcore.DefaultLineEnding, 42 | EncodeLevel: zapcore.LowercaseLevelEncoder, 43 | EncodeTime: zapcore.ISO8601TimeEncoder, 44 | EncodeDuration: zapcore.StringDurationEncoder, 45 | EncodeCaller: zapcore.ShortCallerEncoder, 46 | }, 47 | OutputPaths: []string{"stderr"}, 48 | ErrorOutputPaths: []string{"stderr"}, 49 | }.Build() 50 | 51 | // TestMain is needed due to t.Parallel() incompatibility of goleak. 52 | // https://github.com/uber-go/goleak/issues/16 53 | goleak.VerifyTestMain(m, 54 | // controller-runtime's log.init intentionally waits: https://github.com/kubernetes-sigs/controller-runtime/pull/1309 55 | goleak.IgnoreTopFunction("time.Sleep"), 56 | // flushDaemon leaks: https://github.com/kubernetes/client-go/issues/900 57 | goleak.IgnoreTopFunction("k8s.io/klog/v2.(*loggingT).flushDaemon"), 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /controller/certificatesigningrequest/controller.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Alex Szakaly 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | package certificatesigningrequest 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "fmt" 22 | "time" 23 | 24 | "go.uber.org/zap" 25 | authorizationv1 "k8s.io/api/authorization/v1" 26 | certificatesv1 "k8s.io/api/certificates/v1" 27 | corev1 "k8s.io/api/core/v1" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "k8s.io/apimachinery/pkg/runtime" 30 | k8sclient "k8s.io/client-go/kubernetes" 31 | "k8s.io/client-go/tools/record" 32 | ctrl "sigs.k8s.io/controller-runtime" 33 | ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 34 | 35 | "github.com/alex1989hu/kubelet-serving-cert-approver/metrics" 36 | ) 37 | 38 | const eventWarningReason = "ApproveFailed" 39 | 40 | var errSubjectAccessReview = errors.New("could not perform Subject Access Review") 41 | 42 | // SigningReconciler reconciles a CertificateSigningRequest object. 43 | type SigningReconciler struct { 44 | Client ctrlclient.Client 45 | ClientSet k8sclient.Interface 46 | Scheme *runtime.Scheme 47 | EventRecorder record.EventRecorder 48 | Logger *zap.Logger 49 | } 50 | 51 | // Reconcile processes request and returns the result. 52 | // 53 | //nolint:gocyclo 54 | func (r *SigningReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 55 | reqLogger := r.Logger.With(zap.String("csr.name", req.Name)) 56 | 57 | var csr certificatesv1.CertificateSigningRequest 58 | 59 | if err := r.Client.Get(ctx, req.NamespacedName, &csr); ctrlclient.IgnoreNotFound(err) != nil { 60 | message := "Unable to to get Certificate Signing Requests" 61 | reqLogger.Error(message, zap.Error(err)) 62 | 63 | return ctrl.Result{}, fmt.Errorf("%s %w", message, err) 64 | } 65 | 66 | switch { 67 | case csr.Spec.SignerName != certificatesv1.KubeletServingSignerName: 68 | if ce := reqLogger.Check(zap.DebugLevel, 69 | "Certificate Signing Request is not Kubelet serving Certificate"); ce != nil { 70 | ce.Write( 71 | zap.String("csr.signer", csr.Spec.SignerName), 72 | ) 73 | } 74 | case !csr.DeletionTimestamp.IsZero(): 75 | if ce := reqLogger.Check(zap.DebugLevel, 76 | "Certificate Signing Request has been deleted"); ce != nil { 77 | ce.Write( 78 | zap.Time("csr.deleted", csr.DeletionTimestamp.Time), 79 | ) 80 | } 81 | case csr.Status.Certificate != nil: 82 | if ce := reqLogger.Check(zap.DebugLevel, 83 | "Certificate Signing Request is already signed"); ce != nil { 84 | ce.Write( 85 | zap.String("csr.signer", csr.Spec.SignerName), 86 | ) 87 | } 88 | case len(csr.Status.Conditions) != 0: 89 | if ce := reqLogger.Check(zap.DebugLevel, 90 | "Certificate Signing Request already has approval condition"); ce != nil { 91 | ce.Write( 92 | zap.Any("csr.conditions", csr.Status.Conditions), 93 | ) 94 | } 95 | default: 96 | x509cr, err := parseCSR(csr.Spec.Request) 97 | if err != nil { 98 | message := "Unable to parse Certificate Signing Request" 99 | 100 | reqLogger.Error(message, zap.Error(err)) 101 | metrics.NumberOfInvalidCertificateSigningRequests.Inc() 102 | r.EventRecorder.Event(&csr, corev1.EventTypeWarning, eventWarningReason, 103 | message+": "+csr.Name+"): "+err.Error()) 104 | 105 | return ctrl.Result{}, err 106 | } 107 | 108 | if errIsKubeletServingCert := isRequestConform(reqLogger, csr, x509cr); errIsKubeletServingCert != nil { 109 | message := "Unable to recognize the Certificate Signing Request" 110 | 111 | reqLogger.Error(message, zap.Error(errIsKubeletServingCert)) 112 | metrics.NumberOfInvalidCertificateSigningRequests.Inc() 113 | r.EventRecorder.Event(&csr, corev1.EventTypeWarning, eventWarningReason, 114 | message+": "+csr.Name+"): "+errIsKubeletServingCert.Error()) 115 | 116 | return ctrl.Result{}, fmt.Errorf( 117 | "the Certificate Signing Request does not conform with expectation: %w", errIsKubeletServingCert) 118 | } 119 | 120 | authorized, err := r.authorize(&csr) 121 | if err != nil { 122 | message := "Unable to get authorization of Certificate Signing Request" 123 | 124 | reqLogger.Error(message, zap.Error(err)) 125 | metrics.NumberOfInvalidCertificateSigningRequests.Inc() 126 | r.EventRecorder.Event(&csr, corev1.EventTypeWarning, eventWarningReason, 127 | message+": "+csr.Name+err.Error()) 128 | 129 | return ctrl.Result{}, fmt.Errorf("%s: %w", message, err) 130 | } 131 | 132 | if authorized { 133 | appendApprovalCondition(&csr) 134 | 135 | _, err = r.ClientSet.CertificatesV1().CertificateSigningRequests().UpdateApproval( 136 | context.TODO(), csr.Name, &csr, metav1.UpdateOptions{}) 137 | if err != nil { 138 | message := "Unable to perform UpdateApproval" 139 | 140 | reqLogger.Error(message, zap.Error(err)) 141 | metrics.NumberOfInvalidCertificateSigningRequests.Inc() 142 | r.EventRecorder.Event(&csr, corev1.EventTypeWarning, eventWarningReason, 143 | message+"("+csr.Name+"): "+err.Error()) 144 | 145 | return ctrl.Result{}, fmt.Errorf("%s: %w", message, err) 146 | } 147 | 148 | reqLogger.Info("The Certificate Signing Request has been approved", 149 | zap.Strings("csr.request.dns", x509cr.DNSNames), 150 | zap.Any("csr.request.ip", x509cr.IPAddresses)) 151 | 152 | metrics.NumberOfApprovedCertificateRequests.Inc() 153 | 154 | r.EventRecorder.Event(&csr, corev1.EventTypeNormal, "Approved", 155 | "The Certificate Signing Request has been approved: "+csr.Name) 156 | } else { 157 | message := "Node is not authorized. Unable to perform Subject Access Review, " 158 | 159 | reqLogger.Error(message) 160 | metrics.NumberOfInvalidCertificateSigningRequests.Inc() 161 | r.EventRecorder.Event(&csr, corev1.EventTypeWarning, eventWarningReason, 162 | message+": "+csr.Name) 163 | 164 | return ctrl.Result{}, errSubjectAccessReview 165 | } 166 | } 167 | 168 | return ctrl.Result{}, nil 169 | } 170 | 171 | // Validate that the given node has authorization to actually create CSRs. 172 | func (r *SigningReconciler) authorize(csr *certificatesv1.CertificateSigningRequest) (bool, error) { 173 | log := r.Logger.With(zap.String("csr.name", csr.Name)) 174 | 175 | extra := make(map[string]authorizationv1.ExtraValue, len(csr.Spec.Extra)) 176 | 177 | for k, v := range csr.Spec.Extra { 178 | extra[k] = authorizationv1.ExtraValue(v) 179 | } 180 | 181 | sar := authorizationv1.SubjectAccessReview{ 182 | Spec: authorizationv1.SubjectAccessReviewSpec{ 183 | User: csr.Spec.Username, 184 | UID: csr.Spec.UID, 185 | Groups: csr.Spec.Groups, 186 | Extra: extra, 187 | ResourceAttributes: &authorizationv1.ResourceAttributes{ 188 | Group: certificatesv1.GroupName, 189 | Resource: "certificatesigningrequests", 190 | Verb: "create", 191 | }, 192 | }, 193 | } 194 | 195 | res, err := r.ClientSet.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), &sar, metav1.CreateOptions{}) 196 | if err != nil { 197 | log.Error("Can not create SubjectAccessReviews resource", zap.Error(err)) 198 | 199 | return false, fmt.Errorf("can not perform Subject Access Review action: %w", err) 200 | } 201 | 202 | return res.Status.Allowed, nil 203 | } 204 | 205 | // SetupWithManager configures controller for manager to handle CertificateSigningRequest. 206 | func (r *SigningReconciler) SetupWithManager(mgr ctrl.Manager) error { 207 | return ctrl.NewControllerManagedBy(mgr). //nolint:wrapcheck 208 | For(&certificatesv1.CertificateSigningRequest{}). 209 | Complete(r) 210 | } 211 | 212 | // appendApprovalCondition sets fields for audit purpose. 213 | func appendApprovalCondition(csr *certificatesv1.CertificateSigningRequest) { 214 | csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1.CertificateSigningRequestCondition{ 215 | Type: certificatesv1.CertificateApproved, 216 | Status: corev1.ConditionTrue, 217 | Reason: "Approved by Kubelet Serving Certificate Approver", 218 | LastUpdateTime: metav1.Time{Time: time.Now().UTC()}, 219 | Message: "Auto approving Kubelet Serving Certificate after Subject Access Review.", 220 | }) 221 | } 222 | -------------------------------------------------------------------------------- /controller/certificatesigningrequest/controller_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Alex Szakaly 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | //nolint:testpackage,wrapcheck // Need to reach functions. 17 | package certificatesigningrequest 18 | 19 | import ( 20 | "context" 21 | "crypto/rand" 22 | "crypto/rsa" 23 | "crypto/x509" 24 | "crypto/x509/pkix" 25 | "encoding/pem" 26 | "errors" 27 | "fmt" 28 | "testing" 29 | "time" 30 | 31 | "github.com/stretchr/testify/assert" 32 | "github.com/stretchr/testify/mock" 33 | "github.com/stretchr/testify/require" 34 | authorizationv1 "k8s.io/api/authorization/v1" 35 | certificatesv1 "k8s.io/api/certificates/v1" 36 | k8serrors "k8s.io/apimachinery/pkg/api/errors" 37 | "k8s.io/apimachinery/pkg/api/meta" 38 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 39 | "k8s.io/apimachinery/pkg/runtime" 40 | "k8s.io/apimachinery/pkg/runtime/schema" 41 | "k8s.io/apimachinery/pkg/types" 42 | "k8s.io/client-go/kubernetes/fake" 43 | clientgotesting "k8s.io/client-go/testing" 44 | "k8s.io/client-go/tools/record" 45 | "sigs.k8s.io/controller-runtime/pkg/client" 46 | ctrlfake "sigs.k8s.io/controller-runtime/pkg/client/fake" 47 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 48 | ) 49 | 50 | const ( 51 | name = "csr-e7dbe" 52 | namespace = "test-namespace" 53 | ) 54 | 55 | //nolint:gochecknoglobals 56 | var ( 57 | // rsaPrivateKey provides RSA private key for Certificate Signing Request generation. 58 | rsaPrivateKey *rsa.PrivateKey 59 | 60 | // errMockGet defines an error during mocked client-go Get(...) function. 61 | errMockGet = errors.New("mocked get error") 62 | 63 | // errAuthorization defines an error during mocked Subject Access Review. 64 | errAuthorization = errors.New("mocked authorization error") 65 | 66 | // errApprovalUpdate defines an error during Certificate Signing Request Approval update. 67 | errApprovalUpdate = errors.New("mocked update error") 68 | ) 69 | 70 | // Client is a mock for the controller-runtime dynamic client interface. 71 | type Client struct { 72 | mapper meta.RESTMapper 73 | scheme *runtime.Scheme 74 | StatusMock *StatusClient 75 | mock.Mock 76 | } 77 | 78 | func (c *Client) IsObjectNamespaced(obj runtime.Object) (bool, error) { 79 | args := c.Called(obj) 80 | 81 | return args.Get(0).(bool), args.Error(1) //nolint:errcheck 82 | } 83 | 84 | func (c *Client) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { 85 | args := c.Called(obj) 86 | 87 | return args.Get(0).(schema.GroupVersionKind), args.Error(1) //nolint:errcheck 88 | } 89 | 90 | func (c *Client) SubResource(subResource string) client.SubResourceClient { 91 | args := c.Called(subResource) 92 | 93 | return args.Get(0).(client.SubResourceClient) //nolint:errcheck 94 | } 95 | 96 | // NewMockClient creates a new mock controller-runtime client. 97 | func NewMockClient() *Client { 98 | return &Client{ 99 | StatusMock: &StatusClient{}, 100 | } 101 | } 102 | 103 | // Status fulfills StatusClient interface. 104 | func (c *Client) Status() client.StatusWriter { 105 | return c.StatusMock 106 | } 107 | 108 | // Get fulfills Reader interface. 109 | func (c *Client) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 110 | args := c.Called(ctx, key, obj, opts) 111 | 112 | return args.Error(0) 113 | } 114 | 115 | // List fulfills Reader interface. 116 | func (c *Client) List(context.Context, client.ObjectList, ...client.ListOption) error { 117 | args := c.Called() 118 | 119 | return args.Error(0) 120 | } 121 | 122 | // Create fulfills Writer interface. 123 | func (c *Client) Create(context.Context, client.Object, ...client.CreateOption) error { 124 | args := c.Called() 125 | 126 | return args.Error(0) 127 | } 128 | 129 | // Delete fulfills Writer interface. 130 | func (c *Client) Delete(context.Context, client.Object, ...client.DeleteOption) error { 131 | args := c.Called() 132 | 133 | return args.Error(0) 134 | } 135 | 136 | // Update fulfills Writer interface. 137 | func (c *Client) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 138 | args := c.Called(ctx, obj, opts) 139 | 140 | return args.Error(0) 141 | } 142 | 143 | // Patch fulfills Writer interface. 144 | func (c *Client) Patch(context.Context, client.Object, client.Patch, ...client.PatchOption) error { 145 | args := c.Called() 146 | 147 | return args.Error(0) 148 | } 149 | 150 | // DeleteAllOf fulfills Writer interface. 151 | func (c *Client) DeleteAllOf(context.Context, client.Object, ...client.DeleteAllOfOption) error { 152 | args := c.Called() 153 | 154 | return args.Error(0) 155 | } 156 | 157 | func (c *Client) Scheme() *runtime.Scheme { 158 | return c.scheme 159 | } 160 | 161 | func (c *Client) RESTMapper() meta.RESTMapper { 162 | return c.mapper 163 | } 164 | 165 | type StatusClient struct { 166 | mock.Mock 167 | } 168 | 169 | // Create fulfills SubResourceWriter interface. 170 | func (c *StatusClient) Create(ctx context.Context, obj client.Object, subResource client.Object, 171 | opts ...client.SubResourceCreateOption, 172 | ) error { 173 | args := c.Called(ctx, obj, subResource, opts) 174 | 175 | return args.Error(0) 176 | } 177 | 178 | // Update fulfills SubResourceWriter interface. 179 | func (c *StatusClient) Update(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error { 180 | args := c.Called(ctx, obj, opts) 181 | 182 | return args.Error(0) 183 | } 184 | 185 | // Patch fulfills SubResourceWriter interface. 186 | func (c *StatusClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, 187 | opts ...client.SubResourcePatchOption, 188 | ) error { 189 | args := c.Called(ctx, obj, patch, opts) 190 | 191 | return args.Error(0) 192 | } 193 | 194 | func TestReconcileClientGetError(t *testing.T) { 195 | t.Parallel() 196 | 197 | mockClient := NewMockClient() 198 | 199 | mockClient.On("Get", mock.Anything, types.NamespacedName{}, 200 | mock.Anything, mock.AnythingOfType("[]client.GetOption")).Return(errMockGet).Times(1) 201 | 202 | signingReconciler := &SigningReconciler{Client: mockClient, Scheme: runtime.NewScheme(), Logger: TestLogger} 203 | req := reconcile.Request{NamespacedName: types.NamespacedName{}} 204 | res, err := signingReconciler.Reconcile(t.Context(), req) 205 | 206 | require.Error(t, err) 207 | assert.Contains(t, err.Error(), errMockGet.Error()) 208 | assert.Equal(t, reconcile.Result{}, res) 209 | 210 | mockClient.AssertExpectations(t) 211 | } 212 | 213 | func TestReconcileClientGetNotFoundError(t *testing.T) { 214 | t.Parallel() 215 | 216 | mockClient := NewMockClient() 217 | errNotFound := k8serrors.NewNotFound(schema.GroupResource{Group: "group", Resource: "resource"}, "bela") 218 | 219 | mockClient.On("Get", mock.Anything, types.NamespacedName{}, 220 | mock.Anything, mock.AnythingOfType("[]client.GetOption")).Return(errNotFound).Times(1) 221 | 222 | signingReconciler := &SigningReconciler{Client: mockClient, Scheme: runtime.NewScheme(), Logger: TestLogger} 223 | req := reconcile.Request{NamespacedName: types.NamespacedName{}} 224 | res, err := signingReconciler.Reconcile(t.Context(), req) 225 | 226 | require.NoError(t, err) 227 | assert.Equal(t, reconcile.Result{}, res) 228 | 229 | mockClient.AssertExpectations(t) 230 | } 231 | 232 | func TestReconcileSwitchCasesNegativePath(t *testing.T) { 233 | t.Parallel() 234 | 235 | tables := []struct { 236 | goal string 237 | csr certificatesv1.CertificateSigningRequest 238 | }{ 239 | { 240 | goal: "Not Kubelet serving Certificate SigningRequest", 241 | csr: certificatesv1.CertificateSigningRequest{ 242 | Spec: certificatesv1.CertificateSigningRequestSpec{ 243 | SignerName: "foobar", 244 | }, 245 | }, 246 | }, 247 | { 248 | goal: "Deleted CertificateSigningRequest", 249 | csr: certificatesv1.CertificateSigningRequest{ 250 | ObjectMeta: metav1.ObjectMeta{ 251 | DeletionTimestamp: &metav1.Time{ 252 | Time: time.Now().UTC(), 253 | }, 254 | Finalizers: []string{"delete"}, 255 | Name: name, 256 | Namespace: namespace, 257 | }, 258 | Spec: certificatesv1.CertificateSigningRequestSpec{ 259 | SignerName: certificatesv1.KubeletServingSignerName, 260 | }, 261 | }, 262 | }, 263 | { 264 | goal: "Already signed CertificateSigningRequest", 265 | csr: certificatesv1.CertificateSigningRequest{ 266 | ObjectMeta: metav1.ObjectMeta{ 267 | Name: name, 268 | Namespace: namespace, 269 | }, 270 | Spec: certificatesv1.CertificateSigningRequestSpec{ 271 | SignerName: certificatesv1.KubeletServingSignerName, 272 | }, 273 | Status: certificatesv1.CertificateSigningRequestStatus{ 274 | Certificate: []byte(`foo`), 275 | }, 276 | }, 277 | }, 278 | { 279 | goal: "Already has approval condition: approved", 280 | csr: certificatesv1.CertificateSigningRequest{ 281 | ObjectMeta: metav1.ObjectMeta{ 282 | Name: name, 283 | Namespace: namespace, 284 | }, 285 | Spec: certificatesv1.CertificateSigningRequestSpec{ 286 | SignerName: certificatesv1.KubeletServingSignerName, 287 | }, 288 | Status: certificatesv1.CertificateSigningRequestStatus{ 289 | Conditions: []certificatesv1.CertificateSigningRequestCondition{ 290 | { 291 | Type: certificatesv1.CertificateApproved, 292 | }, 293 | }, 294 | }, 295 | }, 296 | }, 297 | { 298 | goal: "Already has approval condition: denied", 299 | csr: certificatesv1.CertificateSigningRequest{ 300 | ObjectMeta: metav1.ObjectMeta{ 301 | Name: name, 302 | Namespace: namespace, 303 | }, 304 | Spec: certificatesv1.CertificateSigningRequestSpec{ 305 | SignerName: certificatesv1.KubeletServingSignerName, 306 | }, 307 | Status: certificatesv1.CertificateSigningRequestStatus{ 308 | Conditions: []certificatesv1.CertificateSigningRequestCondition{ 309 | { 310 | Type: certificatesv1.CertificateDenied, 311 | }, 312 | }, 313 | }, 314 | }, 315 | }, 316 | { 317 | goal: "Already has approval condition: failed", 318 | csr: certificatesv1.CertificateSigningRequest{ 319 | ObjectMeta: metav1.ObjectMeta{ 320 | Name: name, 321 | Namespace: namespace, 322 | }, 323 | Spec: certificatesv1.CertificateSigningRequestSpec{ 324 | SignerName: certificatesv1.KubeletServingSignerName, 325 | }, 326 | Status: certificatesv1.CertificateSigningRequestStatus{ 327 | Conditions: []certificatesv1.CertificateSigningRequestCondition{ 328 | { 329 | Type: certificatesv1.CertificateFailed, 330 | }, 331 | }, 332 | }, 333 | }, 334 | }, 335 | } 336 | 337 | for _, table := range tables { 338 | t.Run(fmt.Sprint(table.goal), func(t *testing.T) { 339 | t.Parallel() 340 | 341 | fakeClient := ctrlfake.NewClientBuilder().WithRuntimeObjects([]runtime.Object{&table.csr}...).Build() 342 | r := &SigningReconciler{ 343 | Client: fakeClient, 344 | Scheme: runtime.NewScheme(), 345 | Logger: TestLogger, 346 | } 347 | req := reconcile.Request{ 348 | NamespacedName: types.NamespacedName{ 349 | Name: table.csr.GetName(), 350 | Namespace: table.csr.GetNamespace(), 351 | }, 352 | } 353 | 354 | res, err := r.Reconcile(t.Context(), req) 355 | 356 | require.NoError(t, err) 357 | assert.Equal(t, reconcile.Result{}, res) 358 | }) 359 | } 360 | } 361 | 362 | func TestReconcileValidCSR(t *testing.T) { 363 | t.Parallel() 364 | 365 | csr := certificatesv1.CertificateSigningRequest{ 366 | ObjectMeta: metav1.ObjectMeta{ 367 | Name: name, 368 | Namespace: namespace, 369 | }, 370 | Spec: certificatesv1.CertificateSigningRequestSpec{ 371 | Extra: map[string]certificatesv1.ExtraValue{ 372 | "Kubernetes Tiers": []string{"control-plane", "worker"}, 373 | }, 374 | Usages: validUsages, 375 | Username: validUsername, 376 | SignerName: certificatesv1.KubeletServingSignerName, 377 | Request: generatePEMEncodedCSR(t), 378 | }, 379 | } 380 | 381 | fakeEventRecorder := record.NewFakeRecorder(1) 382 | fakeClientset := fake.Clientset{} 383 | 384 | // Provide authorization by fake k8s clientset 385 | //nolint:staticcheck 386 | fakeClientset.Fake.PrependReactor( 387 | "create", 388 | "subjectaccessreviews", 389 | func(_ clientgotesting.Action) (handled bool, ret runtime.Object, err error) { 390 | sar := &authorizationv1.SubjectAccessReview{ 391 | Status: authorizationv1.SubjectAccessReviewStatus{ 392 | Allowed: true, 393 | Reason: "test", 394 | }, 395 | } 396 | 397 | return true, sar, nil 398 | }) 399 | 400 | fakeClient := ctrlfake.NewClientBuilder().WithRuntimeObjects([]runtime.Object{&csr}...).Build() 401 | r := &SigningReconciler{ 402 | Client: fakeClient, 403 | ClientSet: &fakeClientset, 404 | EventRecorder: fakeEventRecorder, 405 | Scheme: runtime.NewScheme(), 406 | Logger: TestLogger, 407 | } 408 | req := reconcile.Request{ 409 | NamespacedName: types.NamespacedName{ 410 | Name: csr.GetName(), 411 | Namespace: csr.GetNamespace(), 412 | }, 413 | } 414 | 415 | res, err := r.Reconcile(t.Context(), req) 416 | 417 | require.NoError(t, err) 418 | assert.Equal(t, reconcile.Result{}, res) 419 | assert.Len(t, fakeEventRecorder.Events, 1) 420 | } 421 | 422 | func TestReconcileParseCSRError(t *testing.T) { 423 | t.Parallel() 424 | 425 | csr := certificatesv1.CertificateSigningRequest{ 426 | ObjectMeta: metav1.ObjectMeta{ 427 | Name: name, 428 | Namespace: namespace, 429 | }, 430 | Spec: certificatesv1.CertificateSigningRequestSpec{ 431 | Usages: validUsages, 432 | Username: validUsername, 433 | SignerName: certificatesv1.KubeletServingSignerName, 434 | Request: []byte(`foobar`), 435 | }, 436 | } 437 | 438 | fakeEventRecorder := record.NewFakeRecorder(1) 439 | fakeClientset := fake.Clientset{} 440 | 441 | //nolint:staticcheck 442 | fakeClientset.Fake.PrependReactor( 443 | "create", 444 | "subjectaccessreviews", 445 | func(_ clientgotesting.Action) (handled bool, ret runtime.Object, err error) { 446 | sar := &authorizationv1.SubjectAccessReview{ 447 | Status: authorizationv1.SubjectAccessReviewStatus{ 448 | Allowed: true, 449 | Reason: "test", 450 | }, 451 | } 452 | 453 | return true, sar, nil 454 | }) 455 | 456 | fakeClient := ctrlfake.NewClientBuilder().WithRuntimeObjects([]runtime.Object{&csr}...).Build() 457 | r := &SigningReconciler{ 458 | Client: fakeClient, 459 | ClientSet: &fakeClientset, 460 | EventRecorder: fakeEventRecorder, 461 | Scheme: runtime.NewScheme(), 462 | Logger: TestLogger, 463 | } 464 | 465 | req := reconcile.Request{ 466 | NamespacedName: types.NamespacedName{ 467 | Name: csr.GetName(), 468 | Namespace: csr.GetNamespace(), 469 | }, 470 | } 471 | 472 | res, err := r.Reconcile(t.Context(), req) 473 | 474 | require.Error(t, err) 475 | assert.Contains(t, err.Error(), "PEM Block") 476 | assert.Equal(t, reconcile.Result{}, res) 477 | assert.Len(t, fakeEventRecorder.Events, 1) 478 | } 479 | 480 | func TestReconcileRecognizeError(t *testing.T) { 481 | t.Parallel() 482 | 483 | csr := certificatesv1.CertificateSigningRequest{ 484 | ObjectMeta: metav1.ObjectMeta{ 485 | Name: name, 486 | Namespace: namespace, 487 | }, 488 | Spec: certificatesv1.CertificateSigningRequestSpec{ 489 | Usages: validUsages, 490 | Username: "foo-system", 491 | SignerName: certificatesv1.KubeletServingSignerName, 492 | Request: generatePEMEncodedCSR(t), 493 | }, 494 | } 495 | 496 | fakeEventRecorder := record.NewFakeRecorder(1) 497 | fakeClientset := fake.Clientset{} 498 | 499 | //nolint:staticcheck 500 | fakeClientset.Fake.PrependReactor( 501 | "create", 502 | "subjectaccessreviews", 503 | func(_ clientgotesting.Action) (handled bool, ret runtime.Object, err error) { 504 | return true, &authorizationv1.SubjectAccessReview{}, nil 505 | }) 506 | 507 | fakeClient := ctrlfake.NewClientBuilder().WithRuntimeObjects([]runtime.Object{&csr}...).Build() 508 | r := &SigningReconciler{ 509 | Client: fakeClient, 510 | ClientSet: &fakeClientset, 511 | EventRecorder: fakeEventRecorder, 512 | Scheme: runtime.NewScheme(), 513 | Logger: TestLogger, 514 | } 515 | 516 | req := reconcile.Request{ 517 | NamespacedName: types.NamespacedName{ 518 | Name: csr.GetName(), 519 | Namespace: csr.GetNamespace(), 520 | }, 521 | } 522 | 523 | res, err := r.Reconcile(t.Context(), req) 524 | 525 | require.Error(t, err) 526 | assert.Contains(t, err.Error(), "x509 Common Name") 527 | assert.Equal(t, reconcile.Result{}, res) 528 | assert.Len(t, fakeEventRecorder.Events, 1) 529 | } 530 | 531 | func TestReconcileAuthorizationError(t *testing.T) { 532 | t.Parallel() 533 | 534 | csr := certificatesv1.CertificateSigningRequest{ 535 | ObjectMeta: metav1.ObjectMeta{ 536 | Name: name, 537 | Namespace: namespace, 538 | }, 539 | Spec: certificatesv1.CertificateSigningRequestSpec{ 540 | Usages: validUsages, 541 | Username: validUsername, 542 | SignerName: certificatesv1.KubeletServingSignerName, 543 | Request: generatePEMEncodedCSR(t), 544 | }, 545 | } 546 | 547 | fakeEventRecorder := record.NewFakeRecorder(1) 548 | fakeClientset := fake.Clientset{} 549 | 550 | //nolint:staticcheck 551 | fakeClientset.Fake.PrependReactor( 552 | "create", 553 | "subjectaccessreviews", 554 | func(_ clientgotesting.Action) (handled bool, ret runtime.Object, err error) { 555 | return true, &authorizationv1.SubjectAccessReview{}, errAuthorization 556 | }) 557 | 558 | fakeClient := ctrlfake.NewClientBuilder().WithRuntimeObjects([]runtime.Object{&csr}...).Build() 559 | r := &SigningReconciler{ 560 | Client: fakeClient, 561 | ClientSet: &fakeClientset, 562 | EventRecorder: fakeEventRecorder, 563 | Scheme: runtime.NewScheme(), 564 | Logger: TestLogger, 565 | } 566 | 567 | req := reconcile.Request{ 568 | NamespacedName: types.NamespacedName{ 569 | Name: csr.GetName(), 570 | Namespace: csr.GetNamespace(), 571 | }, 572 | } 573 | 574 | res, err := r.Reconcile(t.Context(), req) 575 | 576 | require.Error(t, err) 577 | assert.Contains(t, err.Error(), errAuthorization.Error()) 578 | assert.Equal(t, reconcile.Result{}, res) 579 | assert.Len(t, fakeEventRecorder.Events, 1) 580 | } 581 | 582 | func TestReconcileAuthorizationDenied(t *testing.T) { 583 | t.Parallel() 584 | 585 | csr := certificatesv1.CertificateSigningRequest{ 586 | ObjectMeta: metav1.ObjectMeta{ 587 | Name: name, 588 | Namespace: namespace, 589 | }, 590 | Spec: certificatesv1.CertificateSigningRequestSpec{ 591 | Usages: validUsages, 592 | Username: validUsername, 593 | SignerName: certificatesv1.KubeletServingSignerName, 594 | Request: generatePEMEncodedCSR(t), 595 | }, 596 | } 597 | 598 | fakeEventRecorder := record.NewFakeRecorder(1) 599 | fakeClientset := fake.Clientset{} 600 | 601 | //nolint:staticcheck 602 | fakeClientset.Fake.PrependReactor( 603 | "create", 604 | "subjectaccessreviews", 605 | func(_ clientgotesting.Action) (handled bool, ret runtime.Object, err error) { 606 | sar := &authorizationv1.SubjectAccessReview{ 607 | Status: authorizationv1.SubjectAccessReviewStatus{ 608 | Allowed: false, 609 | Reason: "test", 610 | }, 611 | } 612 | 613 | return true, sar, nil 614 | }) 615 | 616 | fakeClient := ctrlfake.NewClientBuilder().WithRuntimeObjects([]runtime.Object{&csr}...).Build() 617 | r := &SigningReconciler{ 618 | Client: fakeClient, 619 | ClientSet: &fakeClientset, 620 | EventRecorder: fakeEventRecorder, 621 | Scheme: runtime.NewScheme(), 622 | Logger: TestLogger, 623 | } 624 | 625 | req := reconcile.Request{ 626 | NamespacedName: types.NamespacedName{ 627 | Name: csr.GetName(), 628 | Namespace: csr.GetNamespace(), 629 | }, 630 | } 631 | 632 | res, err := r.Reconcile(t.Context(), req) 633 | 634 | require.Error(t, err) 635 | assert.Contains(t, err.Error(), "Subject Access Review") 636 | assert.Equal(t, reconcile.Result{}, res) 637 | assert.Len(t, fakeEventRecorder.Events, 1) 638 | } 639 | 640 | func TestReconcileUpdateApprovalError(t *testing.T) { 641 | t.Parallel() 642 | 643 | csr := certificatesv1.CertificateSigningRequest{ 644 | ObjectMeta: metav1.ObjectMeta{ 645 | Name: name, 646 | Namespace: namespace, 647 | }, 648 | Spec: certificatesv1.CertificateSigningRequestSpec{ 649 | Usages: validUsages, 650 | Username: validUsername, 651 | SignerName: certificatesv1.KubeletServingSignerName, 652 | Request: generatePEMEncodedCSR(t), 653 | }, 654 | } 655 | 656 | fakeEventRecorder := record.NewFakeRecorder(1) 657 | fakeClientset := fake.Clientset{} 658 | 659 | // Provide authorization by fake k8s clientset 660 | //nolint:staticcheck 661 | fakeClientset.Fake.PrependReactor( 662 | "create", 663 | "subjectaccessreviews", 664 | func(_ clientgotesting.Action) (handled bool, ret runtime.Object, err error) { 665 | sar := &authorizationv1.SubjectAccessReview{ 666 | Status: authorizationv1.SubjectAccessReviewStatus{ 667 | Allowed: true, 668 | Reason: "test", 669 | }, 670 | } 671 | 672 | return true, sar, nil 673 | }) 674 | 675 | //nolint:staticcheck 676 | fakeClientset.Fake.PrependReactor( 677 | "update", 678 | "certificatesigningrequests", 679 | func(_ clientgotesting.Action) (handled bool, ret runtime.Object, err error) { 680 | return true, &certificatesv1.CertificateSigningRequest{}, errApprovalUpdate 681 | }) 682 | 683 | fakeClient := ctrlfake.NewClientBuilder().WithRuntimeObjects([]runtime.Object{&csr}...).Build() 684 | r := &SigningReconciler{ 685 | Client: fakeClient, 686 | ClientSet: &fakeClientset, 687 | EventRecorder: fakeEventRecorder, 688 | Scheme: runtime.NewScheme(), 689 | Logger: TestLogger, 690 | } 691 | 692 | req := reconcile.Request{ 693 | NamespacedName: types.NamespacedName{ 694 | Name: csr.GetName(), 695 | Namespace: csr.GetNamespace(), 696 | }, 697 | } 698 | 699 | res, err := r.Reconcile(t.Context(), req) 700 | 701 | require.Error(t, err) 702 | assert.Contains(t, err.Error(), errApprovalUpdate.Error()) 703 | assert.Equal(t, reconcile.Result{}, res) 704 | assert.Len(t, fakeEventRecorder.Events, 1) 705 | } 706 | 707 | func TestReconcileNilManager(t *testing.T) { 708 | t.Parallel() 709 | 710 | r := SigningReconciler{} 711 | assert.Error(t, r.SetupWithManager(nil)) 712 | } 713 | 714 | // TestAppendApprovalOptions ensures that the approval options will not change accidentally. 715 | func TestAppendApprovalOptions(t *testing.T) { 716 | t.Parallel() 717 | 718 | csr := certificatesv1.CertificateSigningRequest{} 719 | 720 | appendApprovalCondition(&csr) 721 | assert.Len(t, csr.Status.Conditions, 1) 722 | 723 | condition := csr.Status.Conditions[0] 724 | assert.Equal(t, certificatesv1.CertificateApproved, condition.Type) 725 | assert.Contains(t, condition.Reason, "Kubelet Serving Certificate Approver") 726 | assert.Contains(t, condition.Message, "Auto approving Kubelet Serving Certificate") 727 | } 728 | 729 | func init() { 730 | res, err := rsa.GenerateKey(rand.Reader, 2048) 731 | if err != nil { 732 | panic(err) 733 | } 734 | 735 | rsaPrivateKey = res 736 | } 737 | 738 | // generatePEMEncodedCSR creates PEM encoded Certificate Signing Request. 739 | func generatePEMEncodedCSR(t *testing.T) []byte { 740 | t.Helper() 741 | 742 | csrTemplate := x509.CertificateRequest{ 743 | Subject: pkix.Name{ 744 | Organization: []string{validOrganization}, 745 | CommonName: validUsername, 746 | }, 747 | DNSNames: validDNSNames, 748 | SignatureAlgorithm: x509.SHA256WithRSA, 749 | } 750 | 751 | csrCertificate, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, rsaPrivateKey) 752 | if err != nil { 753 | t.Fatalf("Can not create Certificate Request %v", err) 754 | } 755 | 756 | csr := pem.EncodeToMemory(&pem.Block{ 757 | Type: "CERTIFICATE REQUEST", 758 | Bytes: csrCertificate, 759 | }) 760 | 761 | return csr 762 | } 763 | -------------------------------------------------------------------------------- /controller/certificatesigningrequest/helper.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Alex Szakaly 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | package certificatesigningrequest 17 | 18 | import ( 19 | "crypto/x509" 20 | "encoding/pem" 21 | "errors" 22 | "fmt" 23 | "reflect" 24 | "strings" 25 | 26 | "go.uber.org/zap" 27 | certificatesv1 "k8s.io/api/certificates/v1" 28 | ) 29 | 30 | //nolint:lll 31 | var ( 32 | errDNSOrIPMissing = errors.New("either DNS Names or IP Addresses is missing") 33 | errExtraExtensionsPresent = errors.New("emailAddress and URI subjectAltName extensions are forbidden") 34 | errKeyUsageMismatch = errors.New("key usage does not match") 35 | errNotCertificateRequest = errors.New("PEM Block Type must be CERTIFICATE REQUEST") 36 | errOrganizationMismatch = errors.New("organization does not match") 37 | errX509CommonNameMismatch = errors.New("x509 Common Name does not match with Certificate Signing Request Username") 38 | errX509CommonNamePrefixMismatch = errors.New("x509 Common Name does not start with 'system:node'") 39 | ) 40 | 41 | // parseCSR decodes a PEM encoded Certificate Signing Request. 42 | // https://github.com/kubernetes/kubernetes/blob/v1.20.1/pkg/apis/certificates/v1/helpers.go#L26 43 | func parseCSR(pemBytes []byte) (*x509.CertificateRequest, error) { 44 | block, _ := pem.Decode(pemBytes) 45 | 46 | if block == nil || block.Type != "CERTIFICATE REQUEST" { 47 | return nil, errNotCertificateRequest 48 | } 49 | 50 | csr, err := x509.ParseCertificateRequest(block.Bytes) 51 | if err != nil { 52 | return nil, fmt.Errorf("error occurred during parsing of Certificate Signing Request: %w", err) 53 | } 54 | 55 | return csr, nil 56 | } 57 | 58 | // hasExactUsages check the permitted key usages - exactly ["key encipherment", "digital signature", "server auth"] 59 | // for RSA and ["digital signature", "server auth"] for non-RSA certificates. 60 | func hasExactUsages(log *zap.Logger, csr certificatesv1.CertificateSigningRequest) bool { 61 | permittedUsages := [3]certificatesv1.KeyUsage{ 62 | certificatesv1.UsageDigitalSignature, 63 | certificatesv1.UsageServerAuth, 64 | // Optional since Kubernetes v1.27 https://github.com/kubernetes/kubernetes/pull/111660 65 | certificatesv1.UsageKeyEncipherment, 66 | } 67 | 68 | permittedUsagesMap := map[certificatesv1.KeyUsage]struct{}{} 69 | for _, u := range permittedUsages { 70 | permittedUsagesMap[u] = struct{}{} 71 | } 72 | 73 | for _, u := range csr.Spec.Usages { 74 | if _, ok := permittedUsagesMap[u]; !ok { 75 | log.Warn("Found disallowed certificate usage(s)", zap.String("usage", string(u))) 76 | 77 | return false 78 | } 79 | } 80 | 81 | return true 82 | } 83 | 84 | // isRequestConform returns error if the input does not conform with Kubernetes rules. 85 | // Reference: https://k8s.io/docs/reference/access-authn-authz/certificate-signing-requests/#kubernetes-signers 86 | func isRequestConform(log *zap.Logger, csr certificatesv1.CertificateSigningRequest, 87 | x509cr *x509.CertificateRequest, 88 | ) error { 89 | expectedOrg := "system:nodes" 90 | expectedPrefix := "system:node:" 91 | 92 | if !reflect.DeepEqual([]string{expectedOrg}, x509cr.Subject.Organization) { 93 | log.Warn("X509 Organization does not match", 94 | zap.Strings("csr.request.organization", x509cr.Subject.Organization), 95 | zap.String("expected", expectedOrg)) 96 | 97 | return errOrganizationMismatch 98 | } 99 | 100 | if !strings.HasPrefix(x509cr.Subject.CommonName, expectedPrefix) { 101 | log.Warn("X509 Common Name does not start with expected prefix", 102 | zap.String("csr.request.cn", x509cr.Subject.CommonName), 103 | zap.String("expected", expectedPrefix)) 104 | 105 | return errX509CommonNamePrefixMismatch 106 | } 107 | 108 | if csr.Spec.Username != x509cr.Subject.CommonName { 109 | log.Warn("X509 Common Name does not match with Certificate Signing Request Username", 110 | zap.String("csr.username", csr.Spec.Username), 111 | zap.String("csr.request.cn", x509cr.Subject.CommonName)) 112 | 113 | return errX509CommonNameMismatch 114 | } 115 | 116 | if (len(x509cr.EmailAddresses) != 0) || (len(x509cr.URIs) != 0) { 117 | uris := make([]string, 0, len(x509cr.URIs)) 118 | 119 | for _, uri := range x509cr.URIs { 120 | uris = append(uris, uri.String()) 121 | } 122 | 123 | log.Warn("Forbidden EmailAddress or URI subjectAltName extensions are found", 124 | zap.Strings("csr.request.emails", x509cr.EmailAddresses), 125 | zap.Strings("csr.request.uris", uris)) 126 | 127 | return errExtraExtensionsPresent 128 | } 129 | 130 | if (len(x509cr.DNSNames) < 1) && (len(x509cr.IPAddresses) < 1) { 131 | log.Warn("DNSNames or IP Addresses must be present") 132 | 133 | return errDNSOrIPMissing 134 | } 135 | 136 | if !hasExactUsages(log, csr) { 137 | log.Warn("Certificate Signing Request Usages do not match", 138 | zap.Any("csr.usages", csr.Spec.Usages), 139 | ) 140 | 141 | return errKeyUsageMismatch 142 | } 143 | 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /controller/certificatesigningrequest/helper_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Alex Szakaly 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | //nolint:testpackage // Need to reach functions. 17 | package certificatesigningrequest 18 | 19 | import ( 20 | "crypto/x509" 21 | "crypto/x509/pkix" 22 | "fmt" 23 | "net" 24 | "net/url" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | "github.com/stretchr/testify/require" 29 | certificatesv1 "k8s.io/api/certificates/v1" 30 | ) 31 | 32 | const ( 33 | validOrganization = "system:nodes" 34 | validUsername = "system:node:node-01" 35 | ) 36 | 37 | //nolint:gochecknoglobals 38 | var ( 39 | validDNSNames = []string{"foo.bar"} 40 | valdIPAddresses = []net.IP{net.ParseIP("1.2.3.4")} 41 | validUsages = []certificatesv1.KeyUsage{ 42 | certificatesv1.UsageKeyEncipherment, 43 | certificatesv1.UsageDigitalSignature, 44 | certificatesv1.UsageServerAuth, 45 | } 46 | ) 47 | 48 | func TestParseCSR(t *testing.T) { 49 | t.Parallel() 50 | 51 | csr, err := parseCSR(nil) 52 | 53 | require.Error(t, err) 54 | assert.Nil(t, csr) 55 | assert.ErrorIs(t, err, errNotCertificateRequest) 56 | } 57 | 58 | func TestParseCSRMissingBlock(t *testing.T) { 59 | t.Parallel() 60 | 61 | pemCSR := []byte(` 62 | -----BEGIN CERTIFICATE REQUEST----- 63 | -----END CERTIFICATE REQUEST----- 64 | `) 65 | csr, err := parseCSR(pemCSR) 66 | 67 | require.Error(t, err) 68 | assert.Nil(t, csr) 69 | assert.Contains(t, err.Error(), "during parsing of Certificate Signing Request") 70 | } 71 | 72 | func TestParseCSRValidInput(t *testing.T) { 73 | t.Parallel() 74 | 75 | csr, err := parseCSR(generatePEMEncodedCSR(t)) 76 | 77 | require.NoError(t, err) 78 | assert.NotNil(t, csr) 79 | } 80 | 81 | func TestIsRequestConformInvalidSigningRequest(t *testing.T) { 82 | t.Parallel() 83 | 84 | tables := []struct { 85 | expectedError error 86 | x509cr x509.CertificateRequest 87 | csr certificatesv1.CertificateSigningRequest 88 | }{ 89 | { 90 | csr: certificatesv1.CertificateSigningRequest{ 91 | Spec: certificatesv1.CertificateSigningRequestSpec{ 92 | Usages: validUsages, 93 | Username: validUsername, 94 | SignerName: certificatesv1.KubeletServingSignerName, 95 | }, 96 | }, 97 | x509cr: x509.CertificateRequest{ 98 | Subject: pkix.Name{ 99 | CommonName: validUsername, 100 | Organization: []string{"invalid"}, 101 | }, 102 | DNSNames: validDNSNames, 103 | IPAddresses: valdIPAddresses, 104 | }, 105 | expectedError: errOrganizationMismatch, 106 | }, 107 | { 108 | csr: certificatesv1.CertificateSigningRequest{ 109 | Spec: certificatesv1.CertificateSigningRequestSpec{ 110 | Usages: validUsages, 111 | Username: validUsername, 112 | SignerName: certificatesv1.KubeletServingSignerName, 113 | }, 114 | }, 115 | x509cr: x509.CertificateRequest{ 116 | Subject: pkix.Name{ 117 | CommonName: "bad:prefix:node:node-01", 118 | Organization: []string{validOrganization}, 119 | }, 120 | DNSNames: validDNSNames, 121 | IPAddresses: valdIPAddresses, 122 | }, 123 | expectedError: errX509CommonNamePrefixMismatch, 124 | }, 125 | { 126 | csr: certificatesv1.CertificateSigningRequest{ 127 | Spec: certificatesv1.CertificateSigningRequestSpec{ 128 | Usages: validUsages, 129 | Username: validUsername, 130 | SignerName: certificatesv1.KubeletServingSignerName, 131 | }, 132 | }, 133 | x509cr: x509.CertificateRequest{ 134 | Subject: pkix.Name{ 135 | CommonName: validUsername + "mismatch", 136 | Organization: []string{validOrganization}, 137 | }, 138 | DNSNames: validDNSNames, 139 | IPAddresses: valdIPAddresses, 140 | }, 141 | expectedError: errX509CommonNameMismatch, 142 | }, 143 | { 144 | csr: certificatesv1.CertificateSigningRequest{ 145 | Spec: certificatesv1.CertificateSigningRequestSpec{ 146 | Username: validUsername, 147 | Usages: validUsages, 148 | SignerName: certificatesv1.KubeletServingSignerName, 149 | }, 150 | }, 151 | x509cr: x509.CertificateRequest{ 152 | Subject: pkix.Name{ 153 | CommonName: validUsername, 154 | Organization: []string{validOrganization}, 155 | }, 156 | EmailAddresses: []string{"foo@no-reply.bar"}, 157 | }, 158 | expectedError: errExtraExtensionsPresent, 159 | }, 160 | { 161 | csr: certificatesv1.CertificateSigningRequest{ 162 | Spec: certificatesv1.CertificateSigningRequestSpec{ 163 | Username: validUsername, 164 | Usages: validUsages, 165 | SignerName: certificatesv1.KubeletServingSignerName, 166 | }, 167 | }, 168 | x509cr: x509.CertificateRequest{ 169 | Subject: pkix.Name{ 170 | CommonName: validUsername, 171 | Organization: []string{validOrganization}, 172 | }, 173 | URIs: []*url.URL{{Host: "foo.bar.acme"}}, 174 | }, 175 | expectedError: errExtraExtensionsPresent, 176 | }, 177 | { 178 | csr: certificatesv1.CertificateSigningRequest{ 179 | Spec: certificatesv1.CertificateSigningRequestSpec{ 180 | Username: validUsername, 181 | Usages: validUsages, 182 | SignerName: certificatesv1.KubeletServingSignerName, 183 | }, 184 | }, 185 | x509cr: x509.CertificateRequest{ 186 | Subject: pkix.Name{ 187 | CommonName: validUsername, 188 | Organization: []string{validOrganization}, 189 | }, 190 | EmailAddresses: []string{"foo@no-reply.bar"}, 191 | URIs: []*url.URL{{Host: "foo.bar.acme"}}, 192 | }, 193 | expectedError: errExtraExtensionsPresent, 194 | }, 195 | { 196 | csr: certificatesv1.CertificateSigningRequest{ 197 | Spec: certificatesv1.CertificateSigningRequestSpec{ 198 | Username: validUsername, 199 | Usages: validUsages, 200 | SignerName: certificatesv1.KubeletServingSignerName, 201 | }, 202 | }, 203 | x509cr: x509.CertificateRequest{ 204 | Subject: pkix.Name{ 205 | CommonName: validUsername, 206 | Organization: []string{validOrganization}, 207 | }, 208 | }, 209 | expectedError: errDNSOrIPMissing, 210 | }, 211 | { 212 | csr: certificatesv1.CertificateSigningRequest{ 213 | Spec: certificatesv1.CertificateSigningRequestSpec{ 214 | Username: validUsername, 215 | Usages: []certificatesv1.KeyUsage{ 216 | certificatesv1.UsageServerAuth, 217 | certificatesv1.UsageDigitalSignature, 218 | certificatesv1.UsageKeyEncipherment, 219 | certificatesv1.UsageCodeSigning, 220 | }, 221 | SignerName: certificatesv1.KubeletServingSignerName, 222 | }, 223 | }, 224 | x509cr: x509.CertificateRequest{ 225 | Subject: pkix.Name{ 226 | CommonName: validUsername, 227 | Organization: []string{validOrganization}, 228 | }, 229 | DNSNames: validDNSNames, 230 | IPAddresses: valdIPAddresses, 231 | }, 232 | expectedError: errKeyUsageMismatch, 233 | }, 234 | { 235 | csr: certificatesv1.CertificateSigningRequest{ 236 | Spec: certificatesv1.CertificateSigningRequestSpec{ 237 | Username: validUsername, 238 | Usages: []certificatesv1.KeyUsage{ 239 | certificatesv1.UsageKeyEncipherment, 240 | certificatesv1.UsageDigitalSignature, 241 | certificatesv1.UsageCodeSigning, 242 | }, 243 | SignerName: certificatesv1.KubeletServingSignerName, 244 | }, 245 | }, 246 | x509cr: x509.CertificateRequest{ 247 | Subject: pkix.Name{ 248 | CommonName: validUsername, 249 | Organization: []string{validOrganization}, 250 | }, 251 | DNSNames: validDNSNames, 252 | IPAddresses: valdIPAddresses, 253 | }, 254 | expectedError: errKeyUsageMismatch, 255 | }, 256 | } 257 | 258 | for _, table := range tables { 259 | t.Run(fmt.Sprint(table.expectedError), func(t *testing.T) { 260 | t.Parallel() 261 | assert.ErrorIs(t, isRequestConform(TestLogger, table.csr, &table.x509cr), table.expectedError) 262 | }) 263 | } 264 | } 265 | 266 | func TestConformantKubeletServingCertificateSigningRequest(t *testing.T) { 267 | t.Parallel() 268 | 269 | tables := []struct { 270 | goal string 271 | csr certificatesv1.CertificateSigningRequest 272 | x509cr x509.CertificateRequest 273 | }{ 274 | { 275 | goal: "Only DNSNames present", 276 | csr: certificatesv1.CertificateSigningRequest{ 277 | Spec: certificatesv1.CertificateSigningRequestSpec{ 278 | Usages: validUsages, 279 | Username: validUsername, 280 | SignerName: certificatesv1.KubeletServingSignerName, 281 | }, 282 | }, 283 | x509cr: x509.CertificateRequest{ 284 | Subject: pkix.Name{ 285 | CommonName: validUsername, 286 | Organization: []string{validOrganization}, 287 | }, 288 | DNSNames: validDNSNames, 289 | }, 290 | }, 291 | { 292 | goal: "Only IPAddresses present", 293 | csr: certificatesv1.CertificateSigningRequest{ 294 | Spec: certificatesv1.CertificateSigningRequestSpec{ 295 | Usages: validUsages, 296 | Username: validUsername, 297 | SignerName: certificatesv1.KubeletServingSignerName, 298 | }, 299 | }, 300 | x509cr: x509.CertificateRequest{ 301 | Subject: pkix.Name{ 302 | CommonName: validUsername, 303 | Organization: []string{validOrganization}, 304 | }, 305 | IPAddresses: valdIPAddresses, 306 | }, 307 | }, 308 | { 309 | goal: "All 3 allowed key usages present", 310 | csr: certificatesv1.CertificateSigningRequest{ 311 | Spec: certificatesv1.CertificateSigningRequestSpec{ 312 | Usages: []certificatesv1.KeyUsage{ 313 | certificatesv1.UsageKeyEncipherment, 314 | certificatesv1.UsageDigitalSignature, 315 | certificatesv1.UsageServerAuth, 316 | }, 317 | Username: validUsername, 318 | SignerName: certificatesv1.KubeletServingSignerName, 319 | }, 320 | }, 321 | x509cr: x509.CertificateRequest{ 322 | Subject: pkix.Name{ 323 | CommonName: validUsername, 324 | Organization: []string{validOrganization}, 325 | }, 326 | DNSNames: validDNSNames, 327 | }, 328 | }, 329 | { 330 | goal: "Only digital signature and server auth key usages present", 331 | csr: certificatesv1.CertificateSigningRequest{ 332 | Spec: certificatesv1.CertificateSigningRequestSpec{ 333 | Usages: []certificatesv1.KeyUsage{ 334 | certificatesv1.UsageKeyEncipherment, 335 | certificatesv1.UsageDigitalSignature, 336 | certificatesv1.UsageServerAuth, 337 | }, 338 | Username: validUsername, 339 | SignerName: certificatesv1.KubeletServingSignerName, 340 | }, 341 | }, 342 | x509cr: x509.CertificateRequest{ 343 | Subject: pkix.Name{ 344 | CommonName: validUsername, 345 | Organization: []string{validOrganization}, 346 | }, 347 | DNSNames: validDNSNames, 348 | }, 349 | }, 350 | } 351 | 352 | for _, table := range tables { 353 | t.Run(fmt.Sprint(table.goal), func(t *testing.T) { 354 | t.Parallel() 355 | assert.NoError(t, isRequestConform(TestLogger, table.csr, &table.x509cr)) 356 | }) 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /deploy/README.md: -------------------------------------------------------------------------------- 1 | # Kubelet Serving Certificate Approver Installation Manifests 2 | 3 | The manifests are automatically generated by `kustomize`. If you have specific need, then you can use `kustomize` to generate your own manifests. 4 | 5 | ## Standalone Installation 6 | 7 | * [standalone-install.yaml](standalone-install.yaml) - installs Kubelet Serving Certificate Approver into `kubelet-serving-cert-approver` namespace following the Principle of Least Privilege. 8 | 9 | In nutshell: 10 | 11 | * Configured Container Security Context 12 | * All capabilities dropped 13 | * Read-only root filesystem 14 | * Runs unprivileged with disallowed privilege escalation 15 | * Configured Pod Security Context 16 | * Runs with non-root user 17 | * No shell, uses [distroless](https://github.com/GoogleContainerTools/distroless) image 18 | * Configured Pod Security Standards 19 | * Applied Pod Security Admission labels onto the namespace with `restristed` Pod Security Standards profile 20 | * Only active when `PodSecurity` feature gate is enabled in your cluster 21 | 22 | ## High Availability Installation 23 | 24 | * [ha-install.yaml](ha-install.yaml) - the same as **Standalone Installation** but with multiple replicas. 25 | 26 | ## How to enable debug logging? 27 | 28 | You can add extra argument to `cert-approver` container: 29 | 30 | ```yaml 31 | containers: 32 | - name: cert-approver 33 | args: 34 | - --debug 35 | ``` 36 | 37 | ## How to create ServiceMonitor for Prometheus Operator? 38 | 39 | You can install `ServiceMonitor` by the following example: 40 | 41 | ```yaml 42 | apiVersion: monitoring.coreos.com/v1 43 | kind: ServiceMonitor 44 | metadata: 45 | name: kubelet-serving-cert-approver 46 | namespace: kubelet-serving-cert-approver 47 | labels: 48 | app.kubernetes.io/instance: kubelet-serving-cert-approver 49 | app.kubernetes.io/name: kubelet-serving-cert-approver 50 | spec: 51 | selector: 52 | matchLabels: 53 | app.kubernetes.io/instance: kubelet-serving-cert-approver 54 | app.kubernetes.io/name: kubelet-serving-cert-approver 55 | endpoints: 56 | - interval: 60s 57 | path: /metrics 58 | port: metrics 59 | namespaceSelector: 60 | matchNames: 61 | - kubelet-serving-cert-approver 62 | ``` 63 | -------------------------------------------------------------------------------- /deploy/base/clusterrolebindings.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Alex Szakaly 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | apiVersion: rbac.authorization.k8s.io/v1 17 | kind: ClusterRoleBinding 18 | metadata: 19 | name: kubelet-serving-cert-approver 20 | labels: 21 | app.kubernetes.io/instance: kubelet-serving-cert-approver 22 | app.kubernetes.io/name: kubelet-serving-cert-approver 23 | roleRef: 24 | apiGroup: rbac.authorization.k8s.io 25 | kind: ClusterRole 26 | name: certificates:kubelet-serving-cert-approver 27 | subjects: 28 | - kind: ServiceAccount 29 | name: kubelet-serving-cert-approver 30 | -------------------------------------------------------------------------------- /deploy/base/clusterroles.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: certificates:kubelet-serving-cert-approver 5 | labels: 6 | app.kubernetes.io/instance: kubelet-serving-cert-approver 7 | app.kubernetes.io/name: kubelet-serving-cert-approver 8 | rules: 9 | - apiGroups: 10 | - certificates.k8s.io 11 | resources: 12 | - certificatesigningrequests 13 | verbs: 14 | - get 15 | - list 16 | - watch 17 | - apiGroups: 18 | - certificates.k8s.io 19 | resources: 20 | - certificatesigningrequests/approval 21 | verbs: 22 | - update 23 | - apiGroups: 24 | - authorization.k8s.io 25 | resources: 26 | - subjectaccessreviews 27 | verbs: 28 | - create 29 | - apiGroups: 30 | - certificates.k8s.io 31 | resources: 32 | - signers 33 | resourceNames: 34 | - kubernetes.io/kubelet-serving 35 | verbs: 36 | - approve 37 | 38 | --- 39 | apiVersion: rbac.authorization.k8s.io/v1 40 | kind: ClusterRole 41 | metadata: 42 | name: events:kubelet-serving-cert-approver 43 | labels: 44 | app.kubernetes.io/instance: kubelet-serving-cert-approver 45 | app.kubernetes.io/name: kubelet-serving-cert-approver 46 | rules: 47 | - apiGroups: 48 | - "" 49 | resources: 50 | - events 51 | verbs: 52 | - create 53 | - patch 54 | -------------------------------------------------------------------------------- /deploy/base/deployments.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: kubelet-serving-cert-approver 5 | labels: 6 | app.kubernetes.io/instance: kubelet-serving-cert-approver 7 | app.kubernetes.io/name: kubelet-serving-cert-approver 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app.kubernetes.io/instance: kubelet-serving-cert-approver 13 | app.kubernetes.io/name: kubelet-serving-cert-approver 14 | template: 15 | metadata: 16 | labels: 17 | app.kubernetes.io/instance: kubelet-serving-cert-approver 18 | app.kubernetes.io/name: kubelet-serving-cert-approver 19 | spec: 20 | affinity: 21 | nodeAffinity: 22 | preferredDuringSchedulingIgnoredDuringExecution: 23 | - weight: 100 24 | preference: 25 | matchExpressions: 26 | - key: node-role.kubernetes.io/master 27 | operator: DoesNotExist 28 | - key: node-role.kubernetes.io/control-plane 29 | operator: DoesNotExist 30 | serviceAccountName: kubelet-serving-cert-approver 31 | securityContext: 32 | runAsUser: 65534 33 | runAsGroup: 65534 34 | fsGroup: 65534 35 | seccompProfile: 36 | type: RuntimeDefault 37 | priorityClassName: system-cluster-critical 38 | tolerations: 39 | - key: node.cloudprovider.kubernetes.io/uninitialized 40 | operator: Exists 41 | effect: NoSchedule 42 | - key: node-role.kubernetes.io/master 43 | operator: Exists 44 | effect: NoSchedule 45 | - key: node-role.kubernetes.io/control-plane 46 | operator: Exists 47 | effect: NoSchedule 48 | containers: 49 | - name: cert-approver 50 | image: ghcr.io/alex1989hu/kubelet-serving-cert-approver:main 51 | imagePullPolicy: Always 52 | args: 53 | - serve 54 | env: 55 | - name: NAMESPACE 56 | valueFrom: 57 | fieldRef: 58 | fieldPath: metadata.namespace 59 | resources: 60 | limits: 61 | cpu: 250m 62 | memory: 32Mi 63 | requests: 64 | cpu: 10m 65 | memory: 16Mi 66 | ports: 67 | - name: health 68 | containerPort: 8080 69 | - name: metrics 70 | containerPort: 9090 71 | livenessProbe: 72 | httpGet: 73 | path: /healthz 74 | port: health 75 | initialDelaySeconds: 6 76 | readinessProbe: 77 | httpGet: 78 | path: /readyz 79 | port: health 80 | initialDelaySeconds: 3 81 | securityContext: 82 | allowPrivilegeEscalation: false 83 | privileged: false 84 | readOnlyRootFilesystem: true 85 | runAsNonRoot: true 86 | capabilities: 87 | drop: 88 | - ALL 89 | -------------------------------------------------------------------------------- /deploy/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - clusterrolebindings.yaml 5 | - clusterroles.yaml 6 | - deployments.yaml 7 | - namespaces.yaml 8 | - rolebindings.yaml 9 | - serviceaccounts.yaml 10 | - services.yaml 11 | -------------------------------------------------------------------------------- /deploy/base/namespaces.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: kubelet-serving-cert-approver 5 | labels: 6 | app.kubernetes.io/instance: kubelet-serving-cert-approver 7 | app.kubernetes.io/name: kubelet-serving-cert-approver 8 | pod-security.kubernetes.io/audit: restricted 9 | pod-security.kubernetes.io/enforce: restricted 10 | pod-security.kubernetes.io/warn: restricted 11 | -------------------------------------------------------------------------------- /deploy/base/rolebindings.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: events:kubelet-serving-cert-approver 5 | labels: 6 | app.kubernetes.io/instance: kubelet-serving-cert-approver 7 | app.kubernetes.io/name: kubelet-serving-cert-approver 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: events:kubelet-serving-cert-approver 12 | subjects: 13 | - kind: ServiceAccount 14 | name: kubelet-serving-cert-approver 15 | -------------------------------------------------------------------------------- /deploy/base/serviceaccounts.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: kubelet-serving-cert-approver 5 | labels: 6 | app.kubernetes.io/instance: kubelet-serving-cert-approver 7 | app.kubernetes.io/name: kubelet-serving-cert-approver 8 | -------------------------------------------------------------------------------- /deploy/base/services.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: kubelet-serving-cert-approver 5 | labels: 6 | app.kubernetes.io/instance: kubelet-serving-cert-approver 7 | app.kubernetes.io/name: kubelet-serving-cert-approver 8 | spec: 9 | selector: 10 | app.kubernetes.io/instance: kubelet-serving-cert-approver 11 | app.kubernetes.io/name: kubelet-serving-cert-approver 12 | ports: 13 | - name: metrics 14 | port: 9090 15 | protocol: TCP 16 | targetPort: metrics 17 | -------------------------------------------------------------------------------- /deploy/ha-install.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | app.kubernetes.io/instance: kubelet-serving-cert-approver 6 | app.kubernetes.io/name: kubelet-serving-cert-approver 7 | pod-security.kubernetes.io/audit: restricted 8 | pod-security.kubernetes.io/enforce: restricted 9 | pod-security.kubernetes.io/warn: restricted 10 | name: kubelet-serving-cert-approver 11 | --- 12 | apiVersion: v1 13 | kind: ServiceAccount 14 | metadata: 15 | labels: 16 | app.kubernetes.io/instance: kubelet-serving-cert-approver 17 | app.kubernetes.io/name: kubelet-serving-cert-approver 18 | name: kubelet-serving-cert-approver 19 | namespace: kubelet-serving-cert-approver 20 | --- 21 | apiVersion: rbac.authorization.k8s.io/v1 22 | kind: Role 23 | metadata: 24 | labels: 25 | app.kubernetes.io/instance: kubelet-serving-cert-approver 26 | app.kubernetes.io/name: kubelet-serving-cert-approver 27 | name: leader-election:kubelet-serving-cert-approver 28 | namespace: kubelet-serving-cert-approver 29 | rules: 30 | - apiGroups: 31 | - "" 32 | resources: 33 | - events 34 | verbs: 35 | - create 36 | - patch 37 | - apiGroups: 38 | - coordination.k8s.io 39 | resources: 40 | - leases 41 | verbs: 42 | - create 43 | - get 44 | - update 45 | --- 46 | apiVersion: rbac.authorization.k8s.io/v1 47 | kind: ClusterRole 48 | metadata: 49 | labels: 50 | app.kubernetes.io/instance: kubelet-serving-cert-approver 51 | app.kubernetes.io/name: kubelet-serving-cert-approver 52 | name: certificates:kubelet-serving-cert-approver 53 | rules: 54 | - apiGroups: 55 | - certificates.k8s.io 56 | resources: 57 | - certificatesigningrequests 58 | verbs: 59 | - get 60 | - list 61 | - watch 62 | - apiGroups: 63 | - certificates.k8s.io 64 | resources: 65 | - certificatesigningrequests/approval 66 | verbs: 67 | - update 68 | - apiGroups: 69 | - authorization.k8s.io 70 | resources: 71 | - subjectaccessreviews 72 | verbs: 73 | - create 74 | - apiGroups: 75 | - certificates.k8s.io 76 | resourceNames: 77 | - kubernetes.io/kubelet-serving 78 | resources: 79 | - signers 80 | verbs: 81 | - approve 82 | --- 83 | apiVersion: rbac.authorization.k8s.io/v1 84 | kind: ClusterRole 85 | metadata: 86 | labels: 87 | app.kubernetes.io/instance: kubelet-serving-cert-approver 88 | app.kubernetes.io/name: kubelet-serving-cert-approver 89 | name: events:kubelet-serving-cert-approver 90 | rules: 91 | - apiGroups: 92 | - "" 93 | resources: 94 | - events 95 | verbs: 96 | - create 97 | - patch 98 | --- 99 | apiVersion: rbac.authorization.k8s.io/v1 100 | kind: RoleBinding 101 | metadata: 102 | labels: 103 | app.kubernetes.io/instance: kubelet-serving-cert-approver 104 | app.kubernetes.io/name: kubelet-serving-cert-approver 105 | name: events:kubelet-serving-cert-approver 106 | namespace: default 107 | roleRef: 108 | apiGroup: rbac.authorization.k8s.io 109 | kind: ClusterRole 110 | name: events:kubelet-serving-cert-approver 111 | subjects: 112 | - kind: ServiceAccount 113 | name: kubelet-serving-cert-approver 114 | namespace: kubelet-serving-cert-approver 115 | --- 116 | apiVersion: rbac.authorization.k8s.io/v1 117 | kind: RoleBinding 118 | metadata: 119 | labels: 120 | app.kubernetes.io/instance: kubelet-serving-cert-approver 121 | app.kubernetes.io/name: kubelet-serving-cert-approver 122 | name: leader-election:kubelet-serving-cert-approver 123 | namespace: kubelet-serving-cert-approver 124 | roleRef: 125 | apiGroup: rbac.authorization.k8s.io 126 | kind: Role 127 | name: leader-election:kubelet-serving-cert-approver 128 | subjects: 129 | - kind: ServiceAccount 130 | name: kubelet-serving-cert-approver 131 | namespace: kubelet-serving-cert-approver 132 | --- 133 | apiVersion: rbac.authorization.k8s.io/v1 134 | kind: ClusterRoleBinding 135 | metadata: 136 | labels: 137 | app.kubernetes.io/instance: kubelet-serving-cert-approver 138 | app.kubernetes.io/name: kubelet-serving-cert-approver 139 | name: kubelet-serving-cert-approver 140 | roleRef: 141 | apiGroup: rbac.authorization.k8s.io 142 | kind: ClusterRole 143 | name: certificates:kubelet-serving-cert-approver 144 | subjects: 145 | - kind: ServiceAccount 146 | name: kubelet-serving-cert-approver 147 | namespace: kubelet-serving-cert-approver 148 | --- 149 | apiVersion: v1 150 | kind: Service 151 | metadata: 152 | labels: 153 | app.kubernetes.io/instance: kubelet-serving-cert-approver 154 | app.kubernetes.io/name: kubelet-serving-cert-approver 155 | name: kubelet-serving-cert-approver 156 | namespace: kubelet-serving-cert-approver 157 | spec: 158 | ports: 159 | - name: metrics 160 | port: 9090 161 | protocol: TCP 162 | targetPort: metrics 163 | selector: 164 | app.kubernetes.io/instance: kubelet-serving-cert-approver 165 | app.kubernetes.io/name: kubelet-serving-cert-approver 166 | --- 167 | apiVersion: apps/v1 168 | kind: Deployment 169 | metadata: 170 | labels: 171 | app.kubernetes.io/instance: kubelet-serving-cert-approver 172 | app.kubernetes.io/name: kubelet-serving-cert-approver 173 | name: kubelet-serving-cert-approver 174 | namespace: kubelet-serving-cert-approver 175 | spec: 176 | replicas: 2 177 | selector: 178 | matchLabels: 179 | app.kubernetes.io/instance: kubelet-serving-cert-approver 180 | app.kubernetes.io/name: kubelet-serving-cert-approver 181 | template: 182 | metadata: 183 | labels: 184 | app.kubernetes.io/instance: kubelet-serving-cert-approver 185 | app.kubernetes.io/name: kubelet-serving-cert-approver 186 | spec: 187 | affinity: 188 | nodeAffinity: 189 | preferredDuringSchedulingIgnoredDuringExecution: 190 | - preference: 191 | matchExpressions: 192 | - key: node-role.kubernetes.io/master 193 | operator: DoesNotExist 194 | - key: node-role.kubernetes.io/control-plane 195 | operator: DoesNotExist 196 | weight: 100 197 | podAntiAffinity: 198 | requiredDuringSchedulingIgnoredDuringExecution: 199 | - labelSelector: 200 | matchLabels: 201 | app.kubernetes.io/instance: kubelet-serving-cert-approver 202 | topologyKey: kubernetes.io/hostname 203 | containers: 204 | - args: 205 | - serve 206 | - --enable-leader-election 207 | env: 208 | - name: NAMESPACE 209 | valueFrom: 210 | fieldRef: 211 | fieldPath: metadata.namespace 212 | image: ghcr.io/alex1989hu/kubelet-serving-cert-approver:main 213 | imagePullPolicy: Always 214 | livenessProbe: 215 | httpGet: 216 | path: /healthz 217 | port: health 218 | initialDelaySeconds: 6 219 | name: cert-approver 220 | ports: 221 | - containerPort: 8080 222 | name: health 223 | - containerPort: 9090 224 | name: metrics 225 | readinessProbe: 226 | httpGet: 227 | path: /readyz 228 | port: health 229 | initialDelaySeconds: 3 230 | resources: 231 | limits: 232 | cpu: 250m 233 | memory: 32Mi 234 | requests: 235 | cpu: 10m 236 | memory: 18Mi 237 | securityContext: 238 | allowPrivilegeEscalation: false 239 | capabilities: 240 | drop: 241 | - ALL 242 | privileged: false 243 | readOnlyRootFilesystem: true 244 | runAsNonRoot: true 245 | priorityClassName: system-cluster-critical 246 | securityContext: 247 | fsGroup: 65534 248 | runAsGroup: 65534 249 | runAsUser: 65534 250 | seccompProfile: 251 | type: RuntimeDefault 252 | serviceAccountName: kubelet-serving-cert-approver 253 | tolerations: 254 | - effect: NoSchedule 255 | key: node.cloudprovider.kubernetes.io/uninitialized 256 | operator: Exists 257 | - effect: NoSchedule 258 | key: node-role.kubernetes.io/master 259 | operator: Exists 260 | - effect: NoSchedule 261 | key: node-role.kubernetes.io/control-plane 262 | operator: Exists 263 | -------------------------------------------------------------------------------- /deploy/ha/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: kubelet-serving-cert-approver 4 | bases: 5 | - ../base/ 6 | patchesJson6902: 7 | - target: 8 | group: rbac.authorization.k8s.io 9 | version: v1 10 | kind: RoleBinding 11 | name: events:kubelet-serving-cert-approver 12 | path: rolebindings-patch.yaml 13 | patchesStrategicMerge: 14 | - overlays/deployments.yaml 15 | resources: 16 | - rolebindings.yaml 17 | - roles.yaml 18 | -------------------------------------------------------------------------------- /deploy/ha/overlays/deployments.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: kubelet-serving-cert-approver 5 | spec: 6 | replicas: 2 7 | template: 8 | spec: 9 | affinity: 10 | podAntiAffinity: 11 | requiredDuringSchedulingIgnoredDuringExecution: 12 | - labelSelector: 13 | matchLabels: 14 | app.kubernetes.io/instance: kubelet-serving-cert-approver 15 | topologyKey: kubernetes.io/hostname 16 | containers: 17 | - name: cert-approver 18 | args: 19 | - serve 20 | - --enable-leader-election 21 | resources: 22 | requests: 23 | memory: 18Mi 24 | -------------------------------------------------------------------------------- /deploy/ha/rolebindings-patch.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /metadata/namespace 3 | value: default 4 | - op: add 5 | path: /subjects/0/namespace 6 | value: kubelet-serving-cert-approver # Apply workaround due to kustomize issue 7 | -------------------------------------------------------------------------------- /deploy/ha/rolebindings.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election:kubelet-serving-cert-approver 5 | labels: 6 | app.kubernetes.io/instance: kubelet-serving-cert-approver 7 | app.kubernetes.io/name: kubelet-serving-cert-approver 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: Role 11 | name: leader-election:kubelet-serving-cert-approver 12 | subjects: 13 | - kind: ServiceAccount 14 | name: kubelet-serving-cert-approver 15 | -------------------------------------------------------------------------------- /deploy/ha/roles.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: leader-election:kubelet-serving-cert-approver 5 | labels: 6 | app.kubernetes.io/instance: kubelet-serving-cert-approver 7 | app.kubernetes.io/name: kubelet-serving-cert-approver 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - events 13 | verbs: 14 | - create 15 | - patch 16 | - apiGroups: 17 | - coordination.k8s.io 18 | resources: 19 | - leases 20 | verbs: 21 | - create 22 | - get 23 | - update 24 | -------------------------------------------------------------------------------- /deploy/standalone-install.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | app.kubernetes.io/instance: kubelet-serving-cert-approver 6 | app.kubernetes.io/name: kubelet-serving-cert-approver 7 | pod-security.kubernetes.io/audit: restricted 8 | pod-security.kubernetes.io/enforce: restricted 9 | pod-security.kubernetes.io/warn: restricted 10 | name: kubelet-serving-cert-approver 11 | --- 12 | apiVersion: v1 13 | kind: ServiceAccount 14 | metadata: 15 | labels: 16 | app.kubernetes.io/instance: kubelet-serving-cert-approver 17 | app.kubernetes.io/name: kubelet-serving-cert-approver 18 | name: kubelet-serving-cert-approver 19 | namespace: kubelet-serving-cert-approver 20 | --- 21 | apiVersion: rbac.authorization.k8s.io/v1 22 | kind: ClusterRole 23 | metadata: 24 | labels: 25 | app.kubernetes.io/instance: kubelet-serving-cert-approver 26 | app.kubernetes.io/name: kubelet-serving-cert-approver 27 | name: certificates:kubelet-serving-cert-approver 28 | rules: 29 | - apiGroups: 30 | - certificates.k8s.io 31 | resources: 32 | - certificatesigningrequests 33 | verbs: 34 | - get 35 | - list 36 | - watch 37 | - apiGroups: 38 | - certificates.k8s.io 39 | resources: 40 | - certificatesigningrequests/approval 41 | verbs: 42 | - update 43 | - apiGroups: 44 | - authorization.k8s.io 45 | resources: 46 | - subjectaccessreviews 47 | verbs: 48 | - create 49 | - apiGroups: 50 | - certificates.k8s.io 51 | resourceNames: 52 | - kubernetes.io/kubelet-serving 53 | resources: 54 | - signers 55 | verbs: 56 | - approve 57 | --- 58 | apiVersion: rbac.authorization.k8s.io/v1 59 | kind: ClusterRole 60 | metadata: 61 | labels: 62 | app.kubernetes.io/instance: kubelet-serving-cert-approver 63 | app.kubernetes.io/name: kubelet-serving-cert-approver 64 | name: events:kubelet-serving-cert-approver 65 | rules: 66 | - apiGroups: 67 | - "" 68 | resources: 69 | - events 70 | verbs: 71 | - create 72 | - patch 73 | --- 74 | apiVersion: rbac.authorization.k8s.io/v1 75 | kind: RoleBinding 76 | metadata: 77 | labels: 78 | app.kubernetes.io/instance: kubelet-serving-cert-approver 79 | app.kubernetes.io/name: kubelet-serving-cert-approver 80 | name: events:kubelet-serving-cert-approver 81 | namespace: default 82 | roleRef: 83 | apiGroup: rbac.authorization.k8s.io 84 | kind: ClusterRole 85 | name: events:kubelet-serving-cert-approver 86 | subjects: 87 | - kind: ServiceAccount 88 | name: kubelet-serving-cert-approver 89 | namespace: kubelet-serving-cert-approver 90 | --- 91 | apiVersion: rbac.authorization.k8s.io/v1 92 | kind: ClusterRoleBinding 93 | metadata: 94 | labels: 95 | app.kubernetes.io/instance: kubelet-serving-cert-approver 96 | app.kubernetes.io/name: kubelet-serving-cert-approver 97 | name: kubelet-serving-cert-approver 98 | roleRef: 99 | apiGroup: rbac.authorization.k8s.io 100 | kind: ClusterRole 101 | name: certificates:kubelet-serving-cert-approver 102 | subjects: 103 | - kind: ServiceAccount 104 | name: kubelet-serving-cert-approver 105 | namespace: kubelet-serving-cert-approver 106 | --- 107 | apiVersion: v1 108 | kind: Service 109 | metadata: 110 | labels: 111 | app.kubernetes.io/instance: kubelet-serving-cert-approver 112 | app.kubernetes.io/name: kubelet-serving-cert-approver 113 | name: kubelet-serving-cert-approver 114 | namespace: kubelet-serving-cert-approver 115 | spec: 116 | ports: 117 | - name: metrics 118 | port: 9090 119 | protocol: TCP 120 | targetPort: metrics 121 | selector: 122 | app.kubernetes.io/instance: kubelet-serving-cert-approver 123 | app.kubernetes.io/name: kubelet-serving-cert-approver 124 | --- 125 | apiVersion: apps/v1 126 | kind: Deployment 127 | metadata: 128 | labels: 129 | app.kubernetes.io/instance: kubelet-serving-cert-approver 130 | app.kubernetes.io/name: kubelet-serving-cert-approver 131 | name: kubelet-serving-cert-approver 132 | namespace: kubelet-serving-cert-approver 133 | spec: 134 | replicas: 1 135 | selector: 136 | matchLabels: 137 | app.kubernetes.io/instance: kubelet-serving-cert-approver 138 | app.kubernetes.io/name: kubelet-serving-cert-approver 139 | template: 140 | metadata: 141 | labels: 142 | app.kubernetes.io/instance: kubelet-serving-cert-approver 143 | app.kubernetes.io/name: kubelet-serving-cert-approver 144 | spec: 145 | affinity: 146 | nodeAffinity: 147 | preferredDuringSchedulingIgnoredDuringExecution: 148 | - preference: 149 | matchExpressions: 150 | - key: node-role.kubernetes.io/master 151 | operator: DoesNotExist 152 | - key: node-role.kubernetes.io/control-plane 153 | operator: DoesNotExist 154 | weight: 100 155 | containers: 156 | - args: 157 | - serve 158 | env: 159 | - name: NAMESPACE 160 | valueFrom: 161 | fieldRef: 162 | fieldPath: metadata.namespace 163 | image: ghcr.io/alex1989hu/kubelet-serving-cert-approver:main 164 | imagePullPolicy: Always 165 | livenessProbe: 166 | httpGet: 167 | path: /healthz 168 | port: health 169 | initialDelaySeconds: 6 170 | name: cert-approver 171 | ports: 172 | - containerPort: 8080 173 | name: health 174 | - containerPort: 9090 175 | name: metrics 176 | readinessProbe: 177 | httpGet: 178 | path: /readyz 179 | port: health 180 | initialDelaySeconds: 3 181 | resources: 182 | limits: 183 | cpu: 250m 184 | memory: 32Mi 185 | requests: 186 | cpu: 10m 187 | memory: 16Mi 188 | securityContext: 189 | allowPrivilegeEscalation: false 190 | capabilities: 191 | drop: 192 | - ALL 193 | privileged: false 194 | readOnlyRootFilesystem: true 195 | runAsNonRoot: true 196 | priorityClassName: system-cluster-critical 197 | securityContext: 198 | fsGroup: 65534 199 | runAsGroup: 65534 200 | runAsUser: 65534 201 | seccompProfile: 202 | type: RuntimeDefault 203 | serviceAccountName: kubelet-serving-cert-approver 204 | tolerations: 205 | - effect: NoSchedule 206 | key: node.cloudprovider.kubernetes.io/uninitialized 207 | operator: Exists 208 | - effect: NoSchedule 209 | key: node-role.kubernetes.io/master 210 | operator: Exists 211 | - effect: NoSchedule 212 | key: node-role.kubernetes.io/control-plane 213 | operator: Exists 214 | -------------------------------------------------------------------------------- /deploy/standalone/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | bases: 4 | - ../base/ 5 | namespace: kubelet-serving-cert-approver 6 | patchesJson6902: 7 | - target: 8 | group: rbac.authorization.k8s.io 9 | version: v1 10 | kind: RoleBinding 11 | name: events:kubelet-serving-cert-approver 12 | path: rolebindings-patch.yaml 13 | -------------------------------------------------------------------------------- /deploy/standalone/rolebindings-patch.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /metadata/namespace 3 | value: default 4 | - op: add 5 | path: /subjects/0/namespace 6 | value: kubelet-serving-cert-approver 7 | -------------------------------------------------------------------------------- /e2e/README.md: -------------------------------------------------------------------------------- 1 | # End-to-End (E2E) 2 | 3 | The tests are bounded to `//go:build e2e` [build tag](https://golang.org/cmd/go/#hdr-Build_constraints) which is not activated by default. 4 | 5 | ## Which technology is being used? 6 | 7 | The application [requirements](features) are being tested with [godog](https://github.com/cucumber/godog/) - official **Cucumber BDD framework for Golang**. 8 | 9 | ## How do I execute End-to-End tests? 10 | 11 | The are multiple options avaialable for test execution. 12 | 13 | ```bash 14 | # in Git Repository root directory 15 | go test -tags e2e -v ./e2e 16 | # in the current directory where this README is also located 17 | go test -tags e2e -v ./... 18 | ``` 19 | 20 | *Notice that the `go test` has an extra argument: `-tags e2e`.* 21 | 22 | ## Test Flakiness 23 | 24 | Kubernetes removes `Events` and `Certificate Signing Requests` which has approval condition due to its `TTL` configuration *(e.g. `--event-ttl`, `--controllers: --csrcleaner`)*. 25 | 26 | Meaning that execution of `e2e` tests can be flaky after a given period of time after approval of `Certificate Signing Request`. 27 | 28 | To overcome this, the easiest way is to recreate/remove your [KinD (Kubernetes in Docker)](https://kind.sigs.k8s.io/) cluster. 29 | 30 | The other option is to override previously mentioned configuration options before starting KiND: 31 | 32 | ```yaml 33 | kubeadmConfigPatches: 34 | - |- 35 | kind: ClusterConfiguration 36 | apiServer: 37 | extraArgs: 38 | # Increase duration of Event Time To Leave (TTL) 39 | "event-ttl": "8h0m0s" 40 | controllerManager: 41 | extraArgs: 42 | # Disable csrcleaner contoller to avoid removal of Certificate Signing Request; keep default KiND options 43 | "controllers": "*,bootstrapsigner,tokencleaner,-csrcleaner" 44 | ``` 45 | 46 | These options are already configured in resources being used by [Contribution Guideline](../CONTRIBUTING.md). 47 | 48 | ## Reference 49 | 50 | * Kubernetes API Server Event TTL: 51 | * Kubernetes Controller Manager Controllers: 52 | -------------------------------------------------------------------------------- /e2e/features/certificatesigningrequest.feature: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Alex Szakaly 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | @csr 17 | @certificatesigningrequest 18 | 19 | Feature: Kubelet Serving TLS Certificate Signing Request Approval 20 | As an administrator 21 | In order to securely communicate with kubelet 22 | I need to have a valid certificate issued by Kubernetes Root Certificate Authority 23 | 24 | Scenario: Kubelet Serving TLS Certificate Signing Requests shall have approval condition 25 | Given there are "kubernetes.io/kubelet-serving" Certificate Signing Requests 26 | Then Certificate Signing Requests shall have approval condition 27 | 28 | Scenario: Kubelet Serving TLS Certificate Signing Requests shall be approved 29 | Given there are "kubernetes.io/kubelet-serving" Certificate Signing Requests 30 | Then Certificate Signing Requests shall be approved 31 | -------------------------------------------------------------------------------- /e2e/features/eventrecorder.feature: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Alex Szakaly 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | @events 17 | 18 | Feature: Kubernetes Event Recording 19 | As an administrator 20 | In order to track Certificate Signing Requests that kubelets use to serve TLS endpoints 21 | I need to be able to get events of Certificate Signing Request approvals 22 | 23 | Scenario: Event reason shall contain application specified reason 24 | Given there are events related to Certificate Signing Requests 25 | Then approval events shall contain "Approved" reason 26 | 27 | Scenario: Event message shall contain application specified message 28 | Given there are events related to Certificate Signing Requests 29 | Then approval events shall have "has been approved" message 30 | -------------------------------------------------------------------------------- /e2e/features/livenessprobe.feature: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Alex Szakaly 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | @healthcheck 17 | @liveness 18 | 19 | Feature: Kubernetes Liveness Probe 20 | As an administrator 21 | In order to track application liveness 22 | I need to be able to have endpoint for liveness probe 23 | 24 | Background: 25 | Given there is a running Pod in namespace "kubelet-serving-cert-approver" with label "app.kubernetes.io/name=kubelet-serving-cert-approver" 26 | And the Pod shall provide "/healthz" endpoint at port 8080 27 | 28 | Scenario: Application shall provide liveness probe endpoint 29 | Then response shall contain "ok" 30 | -------------------------------------------------------------------------------- /e2e/features/metrics.feature: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Alex Szakaly 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | @metrics 17 | @prometheus 18 | 19 | Feature: Prometheus Metrics 20 | As an administrator 21 | In order to track Certificate Signing Requests that kubelets use to serve TLS endpoints 22 | I need to be able to have metrics of Certificate Signing Request approvals 23 | 24 | Background: 25 | Given there is a running Pod in namespace "kubelet-serving-cert-approver" with label "app.kubernetes.io/name=kubelet-serving-cert-approver" 26 | And the Pod shall provide "/metrics" endpoint at port 9090 27 | 28 | Scenario: Application shall provide metrics related to Certificate Signing Request 29 | Then response shall be parseable Prometheus Metrics 30 | And metrics shall contain "kubelet_serving_cert_approver_invalid_certificate_signing_request_count" metric 31 | And metrics shall contain "kubelet_serving_cert_approver_approved_certificate_signing_request_count" metric 32 | -------------------------------------------------------------------------------- /e2e/features/readinessprobe.feature: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Alex Szakaly 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | @healthcheck 17 | @readiness 18 | 19 | Feature: Kubernetes Readiness Probe 20 | As an administrator 21 | In order to track application readiness 22 | I need to be able to have endpoint for readiness probe 23 | 24 | Background: 25 | Given there is a running Pod in namespace "kubelet-serving-cert-approver" with label "app.kubernetes.io/name=kubelet-serving-cert-approver" 26 | And the Pod shall provide "/readyz" endpoint at port 8080 27 | 28 | Scenario: Application shall provide readiness probe endpoint 29 | Then response shall contain "ok" 30 | -------------------------------------------------------------------------------- /e2e/features/shell.feature: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Alex Szakaly 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | @security 17 | @shell 18 | 19 | Feature: Security Hardening 20 | As an administrator 21 | In order to reduce attack surface 22 | I need to not to have shell in the container 23 | 24 | Background: 25 | Given there is a running Pod in namespace "kubelet-serving-cert-approver" with label "app.kubernetes.io/name=kubelet-serving-cert-approver" 26 | 27 | Scenario: Container shall not provide any shell 28 | When I execute command in the running Pod 29 | Then command execution shall report error 30 | And command execution error message shall contain: 31 | """ 32 | failed to exec in container: failed to start exec 33 | """ 34 | 35 | Examples: 36 | | command | 37 | | "" | 38 | | "apt" | 39 | | "bash" | 40 | | "curl" | 41 | | "cut" | 42 | | "echo" | 43 | | "ls" | 44 | | "printf" | 45 | | "sh" | 46 | | "wget" | 47 | | "zsh" | 48 | | "/bin/bash" | 49 | | "/bin/sh" | 50 | | "/bin/zsh" | 51 | | "/usr/bin/bash" | 52 | | "/usr/bin/sh" | 53 | | "/usr/bin/zsh" | 54 | 55 | Scenario: Container shall allow execution of the application itself but nothing besides that 56 | When I execute command "/app/kubelet-serving-cert-approver" in the running Pod 57 | Then command execution shall not report any error 58 | -------------------------------------------------------------------------------- /e2e/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Alex Szakaly 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | //go:build e2e 17 | 18 | package e2e_test 19 | 20 | import ( 21 | "os" 22 | "testing" 23 | 24 | "github.com/cucumber/godog" 25 | "github.com/cucumber/godog/colors" 26 | "github.com/spf13/pflag" 27 | ) 28 | 29 | // opts holds configuration for godog. 30 | // 31 | //nolint:gochecknoglobals 32 | var opts = godog.Options{ 33 | Format: "pretty", 34 | StopOnFailure: true, 35 | Strict: true, 36 | Output: colors.Colored(os.Stdout), 37 | } 38 | 39 | func init() { 40 | godog.BindCommandLineFlags("godog.", &opts) 41 | } 42 | 43 | func TestMain(*testing.M) { 44 | pflag.Parse() 45 | opts.Paths = pflag.Args() 46 | 47 | status := godog.TestSuite{ 48 | Name: "certificatesigningrequest", 49 | ScenarioInitializer: InitializeScenario, 50 | Options: &opts, 51 | }.Run() 52 | 53 | os.Exit(status) 54 | } 55 | -------------------------------------------------------------------------------- /e2e/scenario_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Alex Szakaly 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | //go:build e2e 17 | 18 | //nolint:wrapcheck 19 | package e2e_test 20 | 21 | import ( 22 | "bufio" 23 | "bytes" 24 | "context" 25 | "fmt" 26 | "io" 27 | "net/http" 28 | "net/url" 29 | "strconv" 30 | "strings" 31 | "time" 32 | 33 | "github.com/cucumber/godog" 34 | "github.com/prometheus/common/expfmt" 35 | "github.com/stretchr/testify/assert" 36 | certificatesv1 "k8s.io/api/certificates/v1" 37 | corev1 "k8s.io/api/core/v1" 38 | eventsv1 "k8s.io/api/events/v1" 39 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 40 | clientgokubernetes "k8s.io/client-go/kubernetes" 41 | "k8s.io/client-go/kubernetes/scheme" 42 | "k8s.io/client-go/rest" 43 | "k8s.io/client-go/tools/clientcmd" 44 | "k8s.io/client-go/tools/portforward" 45 | "k8s.io/client-go/tools/remotecommand" 46 | "k8s.io/client-go/transport" 47 | "k8s.io/client-go/transport/spdy" 48 | ) 49 | 50 | const expectationDoesNotMeetMessage = "expectation does not meet" 51 | 52 | // commandResult holds command execution result. 53 | type commandResult struct { 54 | Error error 55 | StdOut string 56 | StdErr string 57 | } 58 | 59 | type ApproverInstance struct { 60 | Clientset *clientgokubernetes.Clientset 61 | RestConfig *rest.Config 62 | Pod corev1.Pod 63 | CommandResult commandResult 64 | CertificateSigningRequestList []certificatesv1.CertificateSigningRequest 65 | Events []eventsv1.Event 66 | Metrics []string 67 | Response []byte 68 | } 69 | 70 | // InitializeScenario sets context and defines steps being used in scenarios. 71 | func InitializeScenario(s *godog.ScenarioContext) { 72 | var instance ApproverInstance 73 | 74 | s.Before(func(ctx context.Context, _ *godog.Scenario) (context.Context, error) { 75 | clientset, restConfig, err := createNewClientSet() 76 | if err != nil { 77 | return ctx, err 78 | } 79 | 80 | instance = ApproverInstance{Clientset: clientset, RestConfig: restConfig} 81 | 82 | return ctx, nil 83 | }) 84 | 85 | // Steps for testing features related to Certificate Signing Request Approval 86 | s.Step(`^there are "([^"]*)" Certificate Signing Requests$`, 87 | instance.thereAreCertificateSigningRequests) 88 | 89 | s.Step(`^Certificate Signing Requests shall have approval condition$`, 90 | instance.certificateSigningRequestsShallHaveApprovalCondition) 91 | 92 | s.Step(`^Certificate Signing Requests shall be approved$`, 93 | instance.certificateSigningRequestsShallBeApproved) 94 | 95 | // Steps for testing features related to Event Recording 96 | s.Step(`^there are events related to Certificate Signing Requests$`, 97 | instance.thereAreEventsRelatedToCertificateSigningRequests) 98 | 99 | s.Step(`^approval events shall contain "([^"]*)" reason$`, 100 | instance.approvalEventsShallContainReason) 101 | 102 | s.Step(`^approval events shall have "([^"]*)" message$`, 103 | instance.approvalEventsShallHaveMessage) 104 | 105 | // Steps for testing features related to Prometheus Metrics 106 | s.Step(`^there is a running Pod in namespace "([^"]*)" with label "([^"]*)"$`, 107 | instance.thereIsARunningPodInNamespaceWithLabel) 108 | 109 | s.Step(`^the Pod shall provide "([^"]*)" endpoint at port (\d+)$`, 110 | instance.thePodShallProvideEndpointAtPort, 111 | ) 112 | 113 | s.Step(`^response shall be parseable Prometheus Metrics$`, 114 | instance.responseShallBeParseablePrometheusMetrics, 115 | ) 116 | 117 | s.Step(`^metrics shall contain "([^"]*)" metric$`, 118 | instance.metricsShallContainMetric) 119 | 120 | // Steps for testing features related to Health Check 121 | s.Step(`^response shall contain "([^"]*)"$`, 122 | instance.responseShallContain, 123 | ) 124 | 125 | // Steps for testing security hardening features 126 | s.Step(`^I execute command "([^"]*)" in the running Pod$`, 127 | instance.iExecuteCommandInTheRunningPod, 128 | ) 129 | 130 | s.Step(`^command execution shall report error$`, 131 | instance.commandExecutionShallReportError, 132 | ) 133 | 134 | s.Step(`^command execution shall not report any error$`, 135 | instance.commandExecutionShallNotReportAnyError, 136 | ) 137 | 138 | s.Step(`^command execution error message shall contain:$`, 139 | instance.commandExecutionErrorMessageShallContain, 140 | ) 141 | } 142 | 143 | // thereAreCertificateSigningRequests ensures that there is already existing Certificate Signing Request. 144 | // +feature: certificatesigningrequest 145 | func (c *ApproverInstance) thereAreCertificateSigningRequests(signer string) error { 146 | csrList, errListCertificates := c.Clientset.CertificatesV1().CertificateSigningRequests().List( 147 | context.TODO(), metav1.ListOptions{ 148 | FieldSelector: "spec.signerName=" + signer, 149 | }) 150 | 151 | if err := assertActual(assert.Nil, errListCertificates); err != nil { 152 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, err) 153 | } 154 | 155 | if err := assertExpectedAndActual(assert.GreaterOrEqual, len(csrList.Items), 156 | 1, "There shall be at least one Certificate Signing Request"); err != nil { 157 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, err) 158 | } 159 | 160 | c.CertificateSigningRequestList = csrList.Items 161 | 162 | return nil 163 | } 164 | 165 | // certificateSigningRequestsShallHaveApprovalCondition ensures that each Certificate Signing Requests have 166 | // approval condition. 167 | // feature: certificatesigningrequest 168 | func (c *ApproverInstance) certificateSigningRequestsShallHaveApprovalCondition() error { 169 | for _, csr := range c.CertificateSigningRequestList { 170 | return assertActual(assert.NotNil, csr.Status.Conditions) 171 | } 172 | 173 | return nil 174 | } 175 | 176 | // certificateSigningRequestsShallBeApproved ensures that each Certificate Signing Requests is approved. 177 | // feature: certificatesigningrequest 178 | func (c *ApproverInstance) certificateSigningRequestsShallBeApproved() error { 179 | for _, csr := range c.CertificateSigningRequestList { 180 | for _, condition := range csr.Status.Conditions { 181 | if err := assertExpectedAndActual(assert.Equal, corev1.ConditionTrue, condition.Status); err != nil { 182 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, err) 183 | } 184 | 185 | if err := assertExpectedAndActual(assert.Equal, certificatesv1.CertificateApproved, condition.Type); err != nil { 186 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, err) 187 | } 188 | } 189 | } 190 | 191 | return nil 192 | } 193 | 194 | // thereAreEventsRelatedToCertificateSigningRequests ensures that there is already existing event 195 | // related to Certificate Signing Request. 196 | // feature: eventrecorder 197 | func (c *ApproverInstance) thereAreEventsRelatedToCertificateSigningRequests() error { 198 | events, errListEvents := c.Clientset.EventsV1().Events("default").List(context.TODO(), metav1.ListOptions{}) 199 | if err := assertActual(assert.Nil, errListEvents); err != nil { 200 | return fmt.Errorf("can not list events: %w", err) 201 | } 202 | 203 | if err := assertExpectedAndActual(assert.GreaterOrEqual, len(events.Items), 1, 204 | "There shall be events"); err != nil { 205 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, err) 206 | } 207 | 208 | var actualEvents []eventsv1.Event 209 | 210 | for _, event := range events.Items { 211 | if event.Regarding.Kind == "CertificateSigningRequest" { 212 | actualEvents = append(actualEvents, event) 213 | } 214 | } 215 | 216 | if err := assertExpectedAndActual(assert.GreaterOrEqual, len(actualEvents), 1, 217 | "There shall be at least one event related to Certificate Signing Request"); err != nil { 218 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, err) 219 | } 220 | 221 | c.Events = actualEvents 222 | 223 | return nil 224 | } 225 | 226 | // approvalEventsShallContainReason ensures that each event shall have a specific reason. 227 | // feature: eventrecorder 228 | func (c *ApproverInstance) approvalEventsShallContainReason(reason string) error { 229 | for _, event := range c.Events { 230 | if err := assertExpectedAndActual(assert.Contains, event.Reason, reason); err != nil { 231 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, err) 232 | } 233 | } 234 | 235 | return nil 236 | } 237 | 238 | // approvalEventsShallHaveMessage ensures that each each event shall have a specific message. 239 | // feature: eventrecorder 240 | func (c *ApproverInstance) approvalEventsShallHaveMessage(message string) error { 241 | for _, event := range c.Events { 242 | if err := assertExpectedAndActual(assert.Contains, event.Note, message); err != nil { 243 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, err) 244 | } 245 | } 246 | 247 | return nil 248 | } 249 | 250 | // thereIsARunningPodInNamespaceWithLabel ensures that there is already running Pod within given namespace 251 | // with given label. 252 | // feature: metrics 253 | func (c *ApproverInstance) thereIsARunningPodInNamespaceWithLabel(namespace, label string) error { 254 | podList, errList := c.Clientset.CoreV1().Pods(namespace).List(context.TODO(), 255 | metav1.ListOptions{ 256 | LabelSelector: label, 257 | }, 258 | ) 259 | 260 | if err := assertActual(assert.Nil, errList); err != nil { 261 | return fmt.Errorf("can not list pods in %s namespace: %w", namespace, errList) 262 | } 263 | 264 | if err := assertExpectedAndActual(assert.GreaterOrEqual, len(podList.Items), 1); err != nil { 265 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, errList) 266 | } 267 | 268 | c.Pod = podList.Items[0] 269 | 270 | return nil 271 | } 272 | 273 | // thePodShallProvideEndpointAtPort ensures that there is existing endpoint at given port. 274 | // feature: healthcheck, metrics 275 | func (c *ApproverInstance) thePodShallProvideEndpointAtPort(endpoint string, port int) error { 276 | resp, errRequest := proxyRequestToPod(c.RestConfig, c.Pod.Namespace, c.Pod.Name, "http", endpoint, port) 277 | 278 | if err := assertActual(assert.Nil, errRequest); err != nil { 279 | return fmt.Errorf("can not list pods in %s namespace: %w", c.Pod.Namespace, err) 280 | } 281 | 282 | c.Response = resp 283 | 284 | return nil 285 | } 286 | 287 | // metricsShallContainMetric ensures that the given Prometheus Metric exist. 288 | // feature: metrics 289 | func (c *ApproverInstance) metricsShallContainMetric(metric string) error { 290 | if err := assertExpectedAndActual(assert.Contains, c.Metrics, metric); err != nil { 291 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, err) 292 | } 293 | 294 | return nil 295 | } 296 | 297 | // responseShallBeParseablePrometheusMetrics ensures that the response is parseable Prometheus Metrics. 298 | // feature: metrics 299 | func (c *ApproverInstance) responseShallBeParseablePrometheusMetrics() error { 300 | metrics, errParse := parseMetricNames(c.Response) 301 | if err := assertActual(assert.Nil, errParse); err != nil { 302 | return fmt.Errorf("can not parse Prometheus metrics: %w", errParse) 303 | } 304 | 305 | c.Metrics = metrics 306 | 307 | return nil 308 | } 309 | 310 | // responseShallContain ensures that the saved response contains given string. 311 | // feature: healthcheck 312 | func (c *ApproverInstance) responseShallContain(expected string) error { 313 | if err := assertActual(assert.NotNil, c.Response); err != nil { 314 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, err) 315 | } 316 | 317 | actualResponseText := string(c.Response) 318 | 319 | if err := assertExpectedAndActual(assert.GreaterOrEqual, len(actualResponseText), 1); err != nil { 320 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, err) 321 | } 322 | 323 | if err := assertExpectedAndActual(assert.Contains, actualResponseText, expected); err != nil { 324 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, err) 325 | } 326 | 327 | return nil 328 | } 329 | 330 | // execOption holds options for command execution. 331 | type execOption struct { 332 | Stdin io.Reader 333 | Namespace string 334 | PodName string 335 | ContainerName string 336 | Command []string 337 | CaptureStdout bool 338 | CaptureStderr bool 339 | } 340 | 341 | // ExecuteCommandInContainer executes command in specified container and return stdout, stderr and error. 342 | func (c *ApproverInstance) ExecuteCommandInContainer(options execOption, 343 | timeout time.Duration, 344 | ) (string, string, error) { 345 | const ( 346 | container = "container" 347 | tty = false 348 | resource = "pods" 349 | subResource = "exec" 350 | ) 351 | 352 | request := c.Clientset.CoreV1().RESTClient().Post(). 353 | Resource(resource). 354 | Name(options.PodName). 355 | Namespace(options.Namespace). 356 | SubResource(subResource). 357 | Param(container, options.ContainerName) 358 | 359 | request.VersionedParams(&corev1.PodExecOptions{ 360 | Container: options.ContainerName, 361 | Command: options.Command, 362 | Stdin: options.Stdin != nil, 363 | Stdout: options.CaptureStdout, 364 | Stderr: options.CaptureStderr, 365 | TTY: tty, 366 | }, scheme.ParameterCodec) 367 | 368 | var stdout, stderr bytes.Buffer 369 | 370 | ctx, cancel := context.WithTimeout(context.TODO(), timeout) 371 | 372 | defer cancel() 373 | 374 | err := execute(ctx, http.MethodPost, request.URL(), c.RestConfig, options.Stdin, &stdout, &stderr, tty) 375 | 376 | return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err 377 | } 378 | 379 | // iExecuteCommandInTheRunningPod executes given command in existing Pod. 380 | // feature: security 381 | func (c *ApproverInstance) iExecuteCommandInTheRunningPod(command string) error { 382 | if err := assertExpectedLenAndActual(assert.Len, c.Pod.Spec.Containers, 1); err != nil { 383 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, err) 384 | } 385 | 386 | stdout, stderr, err := c.ExecuteCommandInContainer(execOption{ 387 | Command: []string{command}, 388 | Namespace: c.Pod.Namespace, 389 | PodName: c.Pod.Name, 390 | ContainerName: c.Pod.Spec.Containers[0].Name, 391 | Stdin: nil, 392 | CaptureStdout: true, 393 | CaptureStderr: true, 394 | }, 30*time.Second) 395 | 396 | c.CommandResult = commandResult{ 397 | StdOut: stdout, 398 | StdErr: stderr, 399 | Error: err, 400 | } 401 | 402 | return nil 403 | } 404 | 405 | // commandExecutionShallReportError ensures that executed command reported error. 406 | // feature: security 407 | func (c *ApproverInstance) commandExecutionShallReportError() error { 408 | if err := assertActual(assert.NotNil, c.CommandResult.Error); err != nil { 409 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, err) 410 | } 411 | 412 | return nil 413 | } 414 | 415 | // commandExecutionShallNotReportAnyError ensures that executed command did not report any error. 416 | // feature: security 417 | func (c *ApproverInstance) commandExecutionShallNotReportAnyError() error { 418 | if err := assertActual(assert.Nil, c.CommandResult.Error); err != nil { 419 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, err) 420 | } 421 | 422 | return nil 423 | } 424 | 425 | // commandExecutionErrorMessageShallContain ensures that executed command error message contains expected string. 426 | // feature: security 427 | func (c *ApproverInstance) commandExecutionErrorMessageShallContain(expected *godog.DocString) error { 428 | if err := assertExpectedAndActual(assert.Contains, c.CommandResult.Error.Error(), expected.Content); err != nil { 429 | return fmt.Errorf("%s: %w", expectationDoesNotMeetMessage, err) 430 | } 431 | 432 | return nil 433 | } 434 | 435 | func parseMetricNames(data []byte) ([]string, error) { 436 | parser := expfmt.TextParser{} 437 | 438 | mfs, err := parser.TextToMetricFamilies(bytes.NewReader(data)) 439 | if err != nil { 440 | return nil, fmt.Errorf("can not parse Prometheus Metrics: %w", err) 441 | } 442 | 443 | ms := make([]string, 0, len(mfs)) 444 | 445 | for key := range mfs { 446 | ms = append(ms, key) 447 | } 448 | 449 | return ms, nil 450 | } 451 | 452 | func proxyRequestToPod(config *rest.Config, namespace, podname, scheme, path string, 453 | port int, 454 | ) ([]byte, error) { 455 | cancel, err := setupForwarding(config, namespace, port, podname) 456 | if err != nil { 457 | return nil, fmt.Errorf("can not setup port forwarding: %w", err) 458 | } 459 | 460 | defer cancel() 461 | 462 | var query string 463 | 464 | if strings.Contains(path, "?") { 465 | elm := strings.SplitN(path, "?", 2) 466 | path = elm[0] 467 | query = elm[1] 468 | } 469 | 470 | reqURL := url.URL{ 471 | Scheme: scheme, 472 | Path: path, 473 | RawQuery: query, 474 | Host: fmt.Sprintf("127.0.0.1:%d", port), 475 | } 476 | 477 | resp, err := sendRequest(config, reqURL.String()) 478 | if err != nil { 479 | return nil, fmt.Errorf("can not send request: %w", err) 480 | } 481 | 482 | defer resp.Body.Close() //nolint:errcheck 483 | 484 | body, err := io.ReadAll(resp.Body) 485 | if err != nil { 486 | return nil, fmt.Errorf("can not read response: %w", err) 487 | } 488 | 489 | return body, nil 490 | } 491 | 492 | func setupForwarding(config *rest.Config, namespace string, port int, 493 | podname string, 494 | ) (cancel func(), err error) { 495 | hostIP := strings.TrimPrefix(config.Host, "https://") 496 | 497 | trans, upgrader, err := spdy.RoundTripperFor(config) 498 | if err != nil { 499 | return noop, fmt.Errorf("can not configure RoundTripper: %w", err) 500 | } 501 | 502 | dialer := spdy.NewDialer( 503 | upgrader, 504 | &http.Client{ 505 | Transport: trans, 506 | }, 507 | http.MethodPost, 508 | &url.URL{ 509 | Scheme: "https", 510 | Path: fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", namespace, podname), 511 | Host: hostIP, 512 | }, 513 | ) 514 | 515 | var berr, bout bytes.Buffer 516 | 517 | buffErr := bufio.NewWriter(&berr) 518 | buffOut := bufio.NewWriter(&bout) 519 | 520 | // stopCh controls the port forwarding lifecycle. When it gets closed the port forward will terminate. 521 | stopCh := make(chan struct{}, 1) 522 | // readyCh communicate when the port forward is ready to get traffic. 523 | readyCh := make(chan struct{}, 1) 524 | 525 | portForwarder, err := portforward.New(dialer, []string{strconv.Itoa(port)}, stopCh, readyCh, buffOut, buffErr) 526 | if err != nil { 527 | return noop, fmt.Errorf("can not create new portforwarding: %w", err) 528 | } 529 | 530 | go func() { 531 | if err := portForwarder.ForwardPorts(); err != nil { 532 | panic(err) 533 | } 534 | }() 535 | <-readyCh 536 | 537 | return func() { 538 | stopCh <- struct{}{} 539 | }, nil 540 | } 541 | 542 | func noop() { 543 | } 544 | 545 | func sendRequest(config *rest.Config, url string) (*http.Response, error) { 546 | tsConfig, err := config.TransportConfig() 547 | if err != nil { 548 | return nil, fmt.Errorf("can not configure transport %w", err) 549 | } 550 | 551 | ts, err := transport.New(tsConfig) 552 | if err != nil { 553 | return nil, fmt.Errorf("can not create new transport %w", err) 554 | } 555 | 556 | client := &http.Client{Transport: ts} 557 | 558 | request, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url, nil) 559 | if err != nil { 560 | return nil, fmt.Errorf("can not create new transport: %w", err) 561 | } 562 | 563 | return client.Do(request) 564 | } 565 | 566 | // execute executes command in given container of a Pod. 567 | func execute(ctx context.Context, method string, url *url.URL, config *rest.Config, stdin io.Reader, 568 | stdout, stderr io.Writer, tty bool, 569 | ) error { 570 | exec, err := remotecommand.NewSPDYExecutor(config, method, url) 571 | if err != nil { 572 | return err 573 | } 574 | 575 | return exec.StreamWithContext(ctx, remotecommand.StreamOptions{ 576 | Stdin: stdin, 577 | Stdout: stdout, 578 | Stderr: stderr, 579 | Tty: tty, 580 | }) 581 | } 582 | 583 | // createNewClientSet creates a client to be used to communicate with Kubernetes API Server. 584 | func createNewClientSet() (*clientgokubernetes.Clientset, *rest.Config, error) { 585 | config, errConfig := clientcmd.NewDefaultClientConfigLoadingRules().Load() 586 | if errConfig != nil { 587 | return nil, nil, fmt.Errorf("can not create new default client loading rules: %w", errConfig) 588 | } 589 | 590 | restConfig, errRestConfig := clientcmd.NewDefaultClientConfig(*config, &clientcmd.ConfigOverrides{}).ClientConfig() 591 | if errRestConfig != nil { 592 | return nil, nil, fmt.Errorf("can not create new default client configuration: %w", errRestConfig) 593 | } 594 | 595 | client, errNewClientset := clientgokubernetes.NewForConfig(restConfig) 596 | if errNewClientset != nil { 597 | return nil, nil, fmt.Errorf("can not create clientset for the given config: %w", errNewClientset) 598 | } 599 | 600 | return client, restConfig, nil 601 | } 602 | -------------------------------------------------------------------------------- /e2e/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Alex Szakaly 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | //go:build e2e 17 | 18 | package e2e_test 19 | 20 | import ( 21 | "fmt" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | // assertExpectedAndActual is a helper function to allow the step function to call 27 | // assertion functions where you want to compare an expected and an actual value. 28 | func assertExpectedAndActual(a expectedAndActualAssertion, 29 | expected, actual interface{}, msgAndArgs ...interface{}, 30 | ) error { 31 | var t asserter 32 | 33 | a(&t, expected, actual, msgAndArgs...) 34 | 35 | return t.err 36 | } 37 | 38 | type expectedAndActualAssertion func(t assert.TestingT, 39 | expected, actual interface{}, msgAndArgs ...interface{}) bool 40 | 41 | // assertActual is a helper function to allow the step function to call 42 | // assertion functions where you want to compare an actual value to a 43 | // predefined state like nil, empty or true/false. 44 | func assertActual(a actualAssertion, actual interface{}) error { 45 | var t asserter 46 | 47 | a(&t, actual) 48 | 49 | return t.err 50 | } 51 | 52 | // assertExpectedLenAndActual is a helper function to allow the step function to call 53 | // assertion functions where you want to compare an actual value to a 54 | // has specific length. 55 | func assertExpectedLenAndActual(a func(t assert.TestingT, object interface{}, 56 | length int, msgAndArgs ...interface{}) bool, actual interface{}, length int, msgAndArgs ...interface{}, 57 | ) error { 58 | var t asserter 59 | 60 | a(&t, actual, length, msgAndArgs) 61 | 62 | return t.err 63 | } 64 | 65 | type actualAssertion func(t assert.TestingT, actual interface{}, msgAndArgs ...interface{}) bool 66 | 67 | // asserter is used to be able to retrieve the error reported by the called assertion. 68 | type asserter struct { 69 | err error 70 | } 71 | 72 | // Errorf is used by the called assertion to report an error. 73 | // 74 | //nolint:err113 75 | func (a *asserter) Errorf(format string, args ...interface{}) { 76 | a.err = fmt.Errorf(format, args...) 77 | } 78 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alex1989hu/kubelet-serving-cert-approver 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/cucumber/godog v0.15.0 9 | github.com/go-logr/zapr v1.3.0 10 | github.com/prometheus/client_golang v1.19.1 11 | github.com/prometheus/common v0.55.0 12 | github.com/spf13/cobra v1.8.1 13 | github.com/spf13/pflag v1.0.5 14 | github.com/spf13/viper v1.19.0 15 | github.com/stretchr/testify v1.10.0 16 | go.uber.org/goleak v1.3.0 17 | go.uber.org/zap v1.27.0 18 | k8s.io/api v0.32.0 19 | k8s.io/apimachinery v0.32.0 20 | k8s.io/client-go v0.32.0 21 | k8s.io/klog/v2 v2.130.1 22 | sigs.k8s.io/controller-runtime v0.20.1 23 | ) 24 | 25 | require ( 26 | github.com/beorn7/perks v1.0.1 // indirect 27 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 28 | github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect 29 | github.com/cucumber/messages/go/v21 v21.0.1 // indirect 30 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 31 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 32 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 33 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 34 | github.com/fsnotify/fsnotify v1.7.0 // indirect 35 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 36 | github.com/go-logr/logr v1.4.2 // indirect 37 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 38 | github.com/go-openapi/jsonreference v0.20.2 // indirect 39 | github.com/go-openapi/swag v0.23.0 // indirect 40 | github.com/gofrs/uuid v4.3.1+incompatible // indirect 41 | github.com/gogo/protobuf v1.3.2 // indirect 42 | github.com/golang/protobuf v1.5.4 // indirect 43 | github.com/google/btree v1.1.3 // indirect 44 | github.com/google/gnostic-models v0.6.8 // indirect 45 | github.com/google/go-cmp v0.6.0 // indirect 46 | github.com/google/gofuzz v1.2.0 // indirect 47 | github.com/google/uuid v1.6.0 // indirect 48 | github.com/gorilla/websocket v1.5.0 // indirect 49 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 50 | github.com/hashicorp/go-memdb v1.3.4 // indirect 51 | github.com/hashicorp/golang-lru v0.5.4 // indirect 52 | github.com/hashicorp/hcl v1.0.0 // indirect 53 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 54 | github.com/josharian/intern v1.0.0 // indirect 55 | github.com/json-iterator/go v1.1.12 // indirect 56 | github.com/magiconair/properties v1.8.7 // indirect 57 | github.com/mailru/easyjson v0.7.7 // indirect 58 | github.com/mitchellh/mapstructure v1.5.0 // indirect 59 | github.com/moby/spdystream v0.5.0 // indirect 60 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 61 | github.com/modern-go/reflect2 v1.0.2 // indirect 62 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 63 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 64 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 65 | github.com/pkg/errors v0.9.1 // indirect 66 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 67 | github.com/prometheus/client_model v0.6.1 // indirect 68 | github.com/prometheus/procfs v0.15.1 // indirect 69 | github.com/sagikazarmark/locafero v0.4.0 // indirect 70 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 71 | github.com/sourcegraph/conc v0.3.0 // indirect 72 | github.com/spf13/afero v1.11.0 // indirect 73 | github.com/spf13/cast v1.6.0 // indirect 74 | github.com/stretchr/objx v0.5.2 // indirect 75 | github.com/subosito/gotenv v1.6.0 // indirect 76 | github.com/x448/float16 v0.8.4 // indirect 77 | go.uber.org/multierr v1.11.0 // indirect 78 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 79 | golang.org/x/net v0.39.0 // indirect 80 | golang.org/x/oauth2 v0.23.0 // indirect 81 | golang.org/x/sync v0.13.0 // indirect 82 | golang.org/x/sys v0.32.0 // indirect 83 | golang.org/x/term v0.31.0 // indirect 84 | golang.org/x/text v0.24.0 // indirect 85 | golang.org/x/time v0.7.0 // indirect 86 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 87 | google.golang.org/protobuf v1.35.1 // indirect 88 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 89 | gopkg.in/inf.v0 v0.9.1 // indirect 90 | gopkg.in/ini.v1 v1.67.0 // indirect 91 | gopkg.in/yaml.v3 v3.0.1 // indirect 92 | k8s.io/apiextensions-apiserver v0.32.0 // indirect 93 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 94 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 95 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 96 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 97 | sigs.k8s.io/yaml v1.4.0 // indirect 98 | ) 99 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 2 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 3 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 8 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 9 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 10 | github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= 11 | github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= 12 | github.com/cucumber/godog v0.15.0 h1:51AL8lBXF3f0cyA5CV4TnJFCTHpgiy+1x1Hb3TtZUmo= 13 | github.com/cucumber/godog v0.15.0/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces= 14 | github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= 15 | github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= 16 | github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 20 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 22 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 23 | github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= 24 | github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 25 | github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= 26 | github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= 27 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 28 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 29 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 30 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 31 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 32 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 33 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 34 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 35 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 36 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 37 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 38 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 39 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 40 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 41 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 42 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 43 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 44 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 45 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 46 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 47 | github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 48 | github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= 49 | github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 50 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 51 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 52 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 53 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 54 | github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= 55 | github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 56 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 57 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 58 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 59 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 60 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 61 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 62 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 63 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 64 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 65 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 66 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 67 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 68 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 69 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 70 | github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 71 | github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= 72 | github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 73 | github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c= 74 | github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= 75 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 76 | github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= 77 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 78 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 79 | github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= 80 | github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 81 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 82 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 83 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 84 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 85 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 86 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 87 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 88 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 89 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 90 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 91 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 92 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 93 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 94 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 95 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 96 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 97 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 98 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 99 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 100 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 101 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 102 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 103 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 104 | github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= 105 | github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= 106 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 107 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 108 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 109 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 110 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 111 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 112 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 113 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= 114 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 115 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 116 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 117 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 118 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 119 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 120 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 121 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 122 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 123 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 124 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 125 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 126 | github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= 127 | github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= 128 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 129 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 130 | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= 131 | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 132 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 133 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 134 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 135 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 136 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 137 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 138 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 139 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 140 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 141 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 142 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 143 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 144 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 145 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 146 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 147 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 148 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 149 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 150 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 151 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 152 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 153 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 154 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 155 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 156 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 157 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 158 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 159 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 160 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 161 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 162 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 163 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 164 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 165 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 166 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 167 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 168 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 169 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 170 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 171 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 172 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 173 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 174 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 175 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 176 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 177 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 178 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 179 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 180 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 181 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 182 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 183 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 184 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 185 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 186 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 187 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 188 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 189 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 190 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 191 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 192 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 193 | golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= 194 | golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 195 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 196 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 197 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 198 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 199 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 200 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 201 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 202 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 203 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 204 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 205 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 206 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 207 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 208 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 209 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 210 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 211 | golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= 212 | golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 213 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 214 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 215 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 216 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 217 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 218 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 219 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 220 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 221 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 222 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 223 | gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= 224 | gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 225 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 226 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 227 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 228 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 229 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 230 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 231 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 232 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 233 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 234 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 235 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 236 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 237 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 238 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 239 | k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= 240 | k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= 241 | k8s.io/apiextensions-apiserver v0.32.0 h1:S0Xlqt51qzzqjKPxfgX1xh4HBZE+p8KKBq+k2SWNOE0= 242 | k8s.io/apiextensions-apiserver v0.32.0/go.mod h1:86hblMvN5yxMvZrZFX2OhIHAuFIMJIZ19bTvzkP+Fmw= 243 | k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= 244 | k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 245 | k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= 246 | k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= 247 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 248 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 249 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= 250 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= 251 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 252 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 253 | sigs.k8s.io/controller-runtime v0.20.1 h1:JbGMAG/X94NeM3xvjenVUaBjy6Ui4Ogd/J5ZtjZnHaE= 254 | sigs.k8s.io/controller-runtime v0.20.1/go.mod h1:BrP3w158MwvB3ZbNpaAcIKkHQ7YGpYnzpoSTZ8E14WU= 255 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 256 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 257 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= 258 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= 259 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 260 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 261 | -------------------------------------------------------------------------------- /hack/CHANGELOG.tpl.md: -------------------------------------------------------------------------------- 1 | {{ range .Versions }} 2 | 3 | ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }} ({{ datetime "2006-01-02" .Tag.Date }}) 4 | 5 | {{ range .CommitGroups -}} 6 | ### {{ .Title }} 7 | 8 | {{ range .Commits -}} 9 | * {{ .Subject }} 10 | {{ end }} 11 | {{ end -}} 12 | 13 | {{- if .RevertCommits -}} 14 | ### Reverts 15 | 16 | {{ range .RevertCommits -}} 17 | * {{ .Revert.Header }} 18 | {{ end }} 19 | {{ end -}} 20 | 21 | {{- if .NoteGroups -}} 22 | {{ range .NoteGroups -}} 23 | ### {{ .Title }} 24 | 25 | {{ range .Notes }} 26 | {{ .Body }} 27 | {{ end }} 28 | {{ end -}} 29 | {{ end -}} 30 | {{ end -}} 31 | -------------------------------------------------------------------------------- /hack/LICENSE.header: -------------------------------------------------------------------------------- 1 | Copyright 2021 Alex Szakaly 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /hack/chglog-config.yaml: -------------------------------------------------------------------------------- 1 | style: github 2 | template: CHANGELOG.tpl.md 3 | info: 4 | title: CHANGELOG 5 | repository_url: https://github.com/alex1989hu/kubelet-serving-cert-approver 6 | options: 7 | commits: 8 | # filters: 9 | # Type: 10 | # - feat 11 | # - fix 12 | # - perf 13 | # - refactor 14 | commit_groups: 15 | # title_maps: 16 | # feat: Features 17 | # fix: Bug Fixes 18 | # perf: Performance Improvements 19 | # refactor: Code Refactoring 20 | header: 21 | pattern: "^(\\w*)\\:\\s(.*)$" 22 | pattern_maps: 23 | - Type 24 | - Subject 25 | notes: 26 | keywords: 27 | - BREAKING CHANGE 28 | -------------------------------------------------------------------------------- /hack/e2e-kind-config.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | kubeadmConfigPatches: 6 | - |- 7 | kind: InitConfiguration 8 | nodeRegistration: 9 | kubeletExtraArgs: 10 | "rotate-server-certificates": "true" 11 | - role: worker 12 | kubeadmConfigPatches: 13 | - |- 14 | kind: JoinConfiguration 15 | nodeRegistration: 16 | kubeletExtraArgs: 17 | "rotate-server-certificates": "true" 18 | kubeadmConfigPatches: 19 | - |- 20 | kind: ClusterConfiguration 21 | apiServer: 22 | extraArgs: 23 | # Increase duration of Event Time To Leave (TTL) 24 | "event-ttl": "8h0m0s" 25 | controllerManager: 26 | extraArgs: 27 | # Disable csrcleaner contoller to avoid removal of Certificate Signing Request; keep default KiND options 28 | "controllers": "*,bootstrapsigner,tokencleaner,-csrcleaner" 29 | -------------------------------------------------------------------------------- /hack/generate-manifests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2021 Alex Szakaly 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | set -eu 19 | 20 | readonly GIT_ROOT="$(CDPATH='' cd -- "$(dirname "$0")/.." && pwd -P)" 21 | 22 | generate::update() { 23 | local -r manifests_dir="${GIT_ROOT}/deploy" 24 | kubectl kustomize "${manifests_dir}/standalone" > "${manifests_dir}/standalone-install.yaml" 25 | kubectl kustomize "${manifests_dir}/ha" > "${manifests_dir}/ha-install.yaml" 26 | } 27 | 28 | main() { 29 | generate::update 30 | } 31 | 32 | main "$@" 33 | -------------------------------------------------------------------------------- /hack/kind-with-registry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Adapted from: 4 | # https://github.com/kubernetes-sigs/kind/commits/master/site/static/examples/kind-with-registry.sh 5 | # 6 | # Copyright 2020 The Kubernetes Project 7 | # 8 | # Licensed under the Apache License, Version 2.0 (the "License"); 9 | # you may not use this file except in compliance with the License. 10 | # You may obtain a copy of the License at 11 | # 12 | # http://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, software 15 | # distributed under the License is distributed on an "AS IS" BASIS, 16 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | # See the License for the specific language governing permissions and 18 | # limitations under the License. 19 | 20 | set -o errexit 21 | 22 | # desired cluster name; default is "kind" 23 | KIND_CLUSTER_NAME="${KIND_CLUSTER_NAME:-kind}" 24 | 25 | kind_version=$(kind version) 26 | kind_network='kind' 27 | reg_name='kind-registry' 28 | reg_port='5000' 29 | case "${kind_version}" in 30 | "kind v0.7."* | "kind v0.6."* | "kind v0.5."*) 31 | kind_network='bridge' 32 | ;; 33 | esac 34 | 35 | # create registry container unless it already exists 36 | running="$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" 37 | if [ "${running}" != 'true' ]; then 38 | docker run \ 39 | -d --restart=always -p "127.0.0.1:${reg_port}:5000" --name "${reg_name}" \ 40 | registry:2 41 | fi 42 | 43 | reg_host="${reg_name}" 44 | if [ "${kind_network}" = "bridge" ]; then 45 | reg_host="$(docker inspect -f '{{.NetworkSettings.IPAddress}}' "${reg_name}")" 46 | fi 47 | echo "Registry Host: ${reg_host}" 48 | 49 | # create a cluster with the local registry enabled in containerd 50 | cat </dev/null || true)" 32 | if [ "${running}" == 'true' ]; then 33 | cid="$(docker inspect -f '{{.ID}}' "${reg_name}")" 34 | echo "> Stopping and deleting Kind Registry container..." 35 | docker stop $cid >/dev/null 36 | docker rm $cid >/dev/null 37 | fi 38 | 39 | echo "> Deleting Kind cluster..." 40 | kind delete cluster --name=$KIND_CLUSTER_NAME 41 | -------------------------------------------------------------------------------- /hack/tilt-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_registry": "ghcr.io", 3 | "image_repository": "alex1989hu" 4 | } 5 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Alex Szakaly 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | package logger 17 | 18 | import ( 19 | "log" 20 | 21 | "github.com/spf13/viper" 22 | "go.uber.org/zap" 23 | "go.uber.org/zap/zapcore" 24 | ) 25 | 26 | // CreateLogger creates zap.Logger. 27 | func CreateLogger() *zap.Logger { 28 | logLevel := func() zap.AtomicLevel { 29 | if viper.GetBool("debug") { 30 | return zap.NewAtomicLevelAt(zapcore.DebugLevel) 31 | } 32 | 33 | return zap.NewAtomicLevelAt(zapcore.InfoLevel) 34 | }() 35 | 36 | zapLogger, err := zap.Config{ 37 | Level: logLevel, 38 | Development: false, 39 | Encoding: "json", 40 | EncoderConfig: zapcore.EncoderConfig{ 41 | TimeKey: "time", 42 | LevelKey: "level", 43 | NameKey: "logger", 44 | CallerKey: "caller", 45 | MessageKey: "msg", 46 | LineEnding: zapcore.DefaultLineEnding, 47 | EncodeLevel: zapcore.LowercaseLevelEncoder, 48 | EncodeTime: zapcore.ISO8601TimeEncoder, 49 | EncodeDuration: zapcore.StringDurationEncoder, 50 | EncodeCaller: zapcore.ShortCallerEncoder, 51 | }, 52 | OutputPaths: []string{"stdout"}, 53 | ErrorOutputPaths: []string{"stderr"}, 54 | }.Build() 55 | if err != nil { 56 | log.Fatalf("error creating zap logger parent %v", err) //nolint:revive // We must immediately exit, deep-exit. 57 | } 58 | 59 | defer zapLogger.Sync() //nolint:errcheck 60 | 61 | return zapLogger 62 | } 63 | -------------------------------------------------------------------------------- /logger/logger_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Alex Szakaly 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | package logger_test 17 | 18 | import ( 19 | "fmt" 20 | "sync" 21 | "testing" 22 | 23 | "github.com/spf13/viper" 24 | "github.com/stretchr/testify/assert" 25 | "go.uber.org/goleak" 26 | "go.uber.org/zap" 27 | 28 | "github.com/alex1989hu/kubelet-serving-cert-approver/logger" 29 | ) 30 | 31 | // viperLock helps viper to avoid viper.Set() concurrency issues - only used in test code. 32 | // 33 | //nolint:gochecknoglobals 34 | var viperLock sync.Mutex 35 | 36 | func TestLoggingConfiguration(t *testing.T) { 37 | t.Parallel() 38 | 39 | tables := []struct { 40 | debugEnabled bool 41 | }{ 42 | { 43 | debugEnabled: false, 44 | }, 45 | { 46 | debugEnabled: true, 47 | }, 48 | } 49 | 50 | for _, table := range tables { 51 | t.Run(fmt.Sprintf("Debug level enabled: (%t)", table.debugEnabled), func(t *testing.T) { 52 | t.Parallel() 53 | 54 | viperLock.Lock() 55 | defer viperLock.Unlock() 56 | 57 | viper.Set("debug", table.debugEnabled) 58 | 59 | zapLog := logger.CreateLogger() 60 | assert.NotNil(t, zapLog) 61 | assert.Equal(t, table.debugEnabled, zapLog.Core().Enabled(zap.DebugLevel)) 62 | }) 63 | } 64 | } 65 | 66 | // TestMain is needed due to t.Parallel() incompatibility of goleak. 67 | // https://github.com/uber-go/goleak/issues/16 68 | func TestMain(m *testing.M) { 69 | // flushDaemon leaks: https://github.com/kubernetes/client-go/issues/900 70 | goleak.VerifyTestMain(m, goleak.IgnoreTopFunction("k8s.io/klog/v2.(*loggingT).flushDaemon")) 71 | } 72 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Alex Szakaly 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | package main 17 | 18 | import "github.com/alex1989hu/kubelet-serving-cert-approver/cmd" 19 | 20 | func main() { 21 | cmd.Execute() 22 | } 23 | -------------------------------------------------------------------------------- /metrics/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Alex Szakaly 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | package metrics 17 | 18 | import ( 19 | prom "github.com/prometheus/client_golang/prometheus" 20 | ) 21 | 22 | //nolint:gochecknoglobals 23 | var ( 24 | // NumberOfApprovedCertificateRequests represents a number of approved certificate signing requests. 25 | NumberOfApprovedCertificateRequests = prom.NewCounter(prom.CounterOpts{ 26 | Name: "kubelet_serving_cert_approver_approved_certificate_signing_request_count", 27 | Help: "The number of approved Certificate Signing Request", 28 | }) 29 | 30 | // NumberOfInvalidCertificateSigningRequests represents a number of invalid certificate signing requests. 31 | NumberOfInvalidCertificateSigningRequests = prom.NewCounter(prom.CounterOpts{ 32 | Name: "kubelet_serving_cert_approver_invalid_certificate_signing_request_count", 33 | Help: "The number of invalid Certificate Signing Request", 34 | }) 35 | ) 36 | --------------------------------------------------------------------------------