├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yaml │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── actions │ ├── exists │ │ └── action.yaml │ └── setup-caches │ │ └── action.yaml ├── configs │ ├── ct.yaml │ └── lintconf.yaml ├── maintainers.yaml └── workflows │ ├── check-actions.yml │ ├── check-commit.yml │ ├── check-pr.yml │ ├── coverage.yml │ ├── docker-build.yml │ ├── docker-publish.yml │ ├── e2e.yml │ ├── helm-publish.yml │ ├── helm-test.yml │ ├── lint.yaml │ ├── releaser.yml │ ├── scorecard.yml │ └── stale.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .ko.yaml ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── SECURITY.md ├── api └── v1beta1 │ ├── clusterresoure.go │ ├── globalproxysettings_types.go │ ├── groupversion_info.go │ ├── proxysettings_types.go │ └── zz_generated.deepcopy.go ├── charts └── capsule-proxy │ ├── .helmignore │ ├── .schema.yaml │ ├── Chart.yaml │ ├── README.md │ ├── README.md.gotmpl │ ├── artifacthub-repo.yml │ ├── ci │ ├── backwards-values.yaml │ ├── cert-manager-values.yaml │ ├── deploy-values.yaml │ ├── ds-values.yaml │ └── webhook-values.yaml │ ├── crds │ ├── capsule.clastix.io_globalproxysettings.yaml │ └── capsule.clastix.io_proxysettings.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── _jobs.tpl │ ├── _pod.tpl │ ├── certgen-job.yaml │ ├── certmanager.yaml │ ├── crd-lifecycle │ │ ├── _helpers.tpl │ │ ├── crds.tpl │ │ ├── job.yaml │ │ ├── rbac.yaml │ │ └── serviceaccount.yaml │ ├── crds.tpl │ ├── daemonset.yaml │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── metrics-rbac.yaml │ ├── metrics-service.yaml │ ├── rbac.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ ├── servicemonitor.yaml │ └── webhooks │ │ ├── certificate.yaml │ │ ├── mutating.yaml │ │ └── service.yaml │ ├── values.schema.json │ └── values.yaml ├── commitlint.config.cjs ├── e2e-legacy ├── curl-http-tests │ ├── 00_root.bats │ └── namespaces │ │ ├── get.bats │ │ └── list.bats ├── curl-https-tests │ ├── 00_root.bats │ └── namespaces │ │ ├── get.bats │ │ └── list.bats ├── kind.yaml ├── kubectl-http-tests │ ├── 00_root.bats │ └── namespaces │ │ └── list.bats ├── kubectl-https-tests │ ├── 00_root.bats │ ├── ingressclasses │ │ ├── delete.bats │ │ ├── get.bats │ │ ├── list.bats │ │ └── update.bats │ ├── metrics │ │ ├── get.bats │ │ └── list.bats │ ├── namespaces │ │ ├── create.bats │ │ └── list.bats │ ├── nodes │ │ ├── delete.bats │ │ ├── get.bats │ │ ├── list.bats │ │ └── update.bats │ ├── priorityclasses │ │ ├── delete.bats │ │ ├── get.bats │ │ ├── list.bats │ │ └── update.bats │ └── storageclasses │ │ ├── delete.bats │ │ ├── get.bats │ │ ├── list.bats │ │ └── update.bats ├── libs │ ├── ingressclass_utils.bash │ ├── namespaces_utils.bash │ ├── poll.bash │ ├── priorityclass_utils.bash │ ├── proxysetting_utils.bash │ ├── rolebinding_utils.bash │ ├── serviceaccount_utils.bash │ ├── storageclass_utils.bash │ └── tenants_utils.bash └── run.bash ├── e2e ├── distro │ ├── flux │ │ └── kustomization.yaml │ └── objects │ │ ├── capsule.flux.yaml │ │ ├── cert-manager.flux.yaml │ │ ├── kustomization.yaml │ │ └── metrics.flux.yaml ├── e2e_suite_test.go ├── global_settings_test.go ├── kind.yaml ├── namespace_test.go ├── suite_client_test.go ├── suite_test.go └── utils_test.go ├── go.mod ├── go.sum ├── hack ├── .gitignore └── boilerplate.go.txt ├── internal ├── controllers │ ├── capsule_configuration.go │ ├── role_bindings.go │ └── watchdog │ │ ├── crds_watcher.go │ │ ├── namespaced_watcher.go │ │ └── utils.go ├── features │ └── features.go ├── indexer │ ├── global_proxy_setting.go │ └── proxy_setting.go ├── labels │ └── managed.go ├── modules │ ├── clusterscoped │ │ ├── get.go │ │ └── list.go │ ├── errors │ │ ├── bad_request.go │ │ ├── error.go │ │ ├── not_allowed.go │ │ └── not_found.go │ ├── ingressclass │ │ ├── get.go │ │ ├── list.go │ │ └── utils.go │ ├── lease │ │ └── get.go │ ├── metric │ │ ├── get.go │ │ └── list.go │ ├── module.go │ ├── namespace │ │ ├── const.go │ │ ├── get.go │ │ ├── list.go │ │ └── post.go │ ├── namespaced │ │ └── catchall.go │ ├── node │ │ ├── get.go │ │ └── list.go │ ├── persistentvolume │ │ ├── get.go │ │ ├── list.go │ │ └── utils.go │ ├── pod │ │ └── get.go │ ├── priorityclass │ │ ├── get.go │ │ ├── list.go │ │ └── utils.go │ ├── runtimeclass │ │ ├── get.go │ │ ├── list.go │ │ └── utils.go │ ├── storageclass │ │ ├── get.go │ │ ├── list.go │ │ └── utils.go │ ├── tenants │ │ ├── const.go │ │ ├── get.go │ │ └── list.go │ └── utils │ │ ├── clusterscope.go │ │ ├── gvk.go │ │ ├── node.go │ │ └── selector.go ├── options │ ├── http.go │ ├── kube.go │ ├── listener.go │ └── server.go ├── request │ ├── authtype.go │ ├── authtype_string.go │ ├── error.go │ ├── http.go │ ├── http_test.go │ ├── impersonation.go │ └── request.go ├── tenant │ ├── operations.go │ └── proxytenant.go ├── utils │ └── gvk.go ├── webhooks │ └── watchdog.go └── webserver │ ├── errors │ └── panic.go │ ├── filter.go │ ├── middleware │ ├── allowed_requests.go │ ├── jwt.go │ ├── metrics.go │ ├── metrics_test.go │ └── user_in_group.go │ ├── utils.go │ └── webserver.go ├── main.go └── renovate.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve Capsule-Proxy 4 | title: '' 5 | labels: blocked-needs-validation, bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | # Bug description 15 | 16 | A clear and concise description of what the bug is. 17 | 18 | # How to reproduce 19 | 20 | Steps to reproduce the behavior: 21 | 22 | # Expected behavior 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | # Logs 27 | 28 | If applicable, please provide logs of `capsule`. 29 | 30 | In a standard stand-alone installation of Capsule, 31 | you'd get this by running `kubectl -n capsule-system logs deploy/capsule-proxy`. 32 | 33 | # Additional context 34 | 35 | - Capsule-Proxy version: (`capsule-proxy --version`) 36 | - Helm Chart version: (`helm list -n capsule-system`) 37 | - Kubernetes version: (`kubectl version`) -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yaml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Chat on Slack 4 | url: https://kubernetes.slack.com/archives/C03GETTJQRL 5 | about: Maybe chatting with the community can help 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature for Capsule-Proxy 4 | title: '' 5 | labels: blocked-needs-validation, feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | 18 | 19 | # Describe the feature 20 | 21 | A clear and concise description of the feature. 22 | 23 | # What would the new user story look like? 24 | 25 | How would the new interaction with Capsule-Proxy look like? 26 | Feel free to add a diagram if that helps explain things. 27 | 28 | # Expected behavior 29 | A clear and concise description of what you expect to happen. -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.github/actions/exists/action.yaml: -------------------------------------------------------------------------------- 1 | name: Checks if an input is defined 2 | 3 | description: Checks if an input is defined and outputs 'true' or 'false'. 4 | 5 | inputs: 6 | value: 7 | description: value to test 8 | required: true 9 | 10 | outputs: 11 | result: 12 | description: outputs 'true' or 'false' if input value is defined or not 13 | value: ${{ steps.check.outputs.result }} 14 | 15 | runs: 16 | using: composite 17 | steps: 18 | - shell: bash 19 | id: check 20 | run: | 21 | echo "result=${{ inputs.value != '' }}" >> $GITHUB_OUTPUT 22 | -------------------------------------------------------------------------------- /.github/actions/setup-caches/action.yaml: -------------------------------------------------------------------------------- 1 | name: Setup caches 2 | 3 | description: Setup caches for go modules and build cache. 4 | 5 | inputs: 6 | build-cache-key: 7 | description: build cache prefix 8 | 9 | runs: 10 | using: composite 11 | steps: 12 | - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 13 | with: 14 | path: ~/go/pkg/mod 15 | key: ${{ runner.os }}-go-pkg-mod-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }} 16 | - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 17 | if: ${{ inputs.build-cache-key }} 18 | with: 19 | path: ~/.cache/go-build 20 | key: ${{ runner.os }}-build-cache-${{ inputs.build-cache-key }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }} 21 | -------------------------------------------------------------------------------- /.github/configs/ct.yaml: -------------------------------------------------------------------------------- 1 | remote: origin 2 | target-branch: main 3 | chart-dirs: 4 | - charts 5 | helm-extra-args: "--timeout 600s" 6 | validate-chart-schema: false 7 | validate-maintainers: false 8 | validate-yaml: true 9 | exclude-deprecated: true 10 | check-version-increment: false 11 | -------------------------------------------------------------------------------- /.github/configs/lintconf.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ignore: 3 | - config/ 4 | - charts/*/templates/ 5 | - charts/**/templates/ 6 | rules: 7 | truthy: 8 | level: warning 9 | allowed-values: 10 | - "true" 11 | - "false" 12 | - "on" 13 | - "off" 14 | check-keys: false 15 | braces: 16 | min-spaces-inside: 0 17 | max-spaces-inside: 0 18 | min-spaces-inside-empty: -1 19 | max-spaces-inside-empty: -1 20 | brackets: 21 | min-spaces-inside: 0 22 | max-spaces-inside: 0 23 | min-spaces-inside-empty: -1 24 | max-spaces-inside-empty: -1 25 | colons: 26 | max-spaces-before: 0 27 | max-spaces-after: 1 28 | commas: 29 | max-spaces-before: 0 30 | min-spaces-after: 1 31 | max-spaces-after: 1 32 | comments: 33 | require-starting-space: true 34 | min-spaces-from-content: 1 35 | document-end: disable 36 | document-start: disable # No --- to start a file 37 | empty-lines: 38 | max: 2 39 | max-start: 0 40 | max-end: 0 41 | hyphens: 42 | max-spaces-after: 1 43 | indentation: 44 | spaces: consistent 45 | indent-sequences: whatever # - list indentation will handle both indentation and without 46 | check-multi-line-strings: false 47 | key-duplicates: enable 48 | line-length: disable # Lines can be any length 49 | new-line-at-end-of-file: enable 50 | new-lines: 51 | type: unix 52 | trailing-spaces: enable 53 | -------------------------------------------------------------------------------- /.github/maintainers.yaml: -------------------------------------------------------------------------------- 1 | - name: Adriano Pezzuto 2 | github: https://github.com/bsctl 3 | company: Clastix 4 | projects: 5 | - https://github.com/projectcapsule/capsule 6 | - https://github.com/projectcapsule/capsule-proxy 7 | - name: Dario Tranchitella 8 | github: https://github.com/prometherion 9 | company: Clastix 10 | projects: 11 | - https://github.com/projectcapsule/capsule 12 | - https://github.com/projectcapsule/capsule-proxy 13 | - name: Maksim Fedotov 14 | github: https://github.com/MaxFedotov 15 | company: wargaming.net 16 | projects: 17 | - https://github.com/projectcapsule/capsule 18 | - https://github.com/projectcapsule/capsule-proxy 19 | - name: Oliver Bähler 20 | github: https://github.com/oliverbaehler 21 | company: Peak Scale 22 | projects: 23 | - https://github.com/projectcapsule/capsule 24 | - https://github.com/projectcapsule/capsule-proxy 25 | - name: Massimiliano Giovagnoli 26 | github: https://github.com/maxgio92 27 | company: Proximus 28 | projects: 29 | - https://github.com/projectcapsule/capsule 30 | - https://github.com/projectcapsule/capsule-proxy 31 | -------------------------------------------------------------------------------- /.github/workflows/check-actions.yml: -------------------------------------------------------------------------------- 1 | name: Check actions 2 | permissions: {} 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - "*" 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | check: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | - name: Ensure SHA pinned actions 20 | uses: zgosalvez/github-actions-ensure-sha-pinned-actions@fc87bb5b5a97953d987372e74478de634726b3e5 # v3.0.25 21 | with: 22 | # slsa-github-generator requires using a semver tag for reusable workflows. 23 | # See: https://github.com/slsa-framework/slsa-github-generator#referencing-slsa-builders-and-generators 24 | allowlist: | 25 | slsa-framework/slsa-github-generator 26 | -------------------------------------------------------------------------------- /.github/workflows/check-commit.yml: -------------------------------------------------------------------------------- 1 | name: Check Commit 2 | permissions: {} 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - "*" 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | commit_lint: 15 | runs-on: ubuntu-24.04 16 | steps: 17 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 18 | with: 19 | fetch-depth: 0 20 | - uses: wagoid/commitlint-github-action@b948419dd99f3fd78a6548d48f94e3df7f6bf3ed # v6.2.1 21 | -------------------------------------------------------------------------------- /.github/workflows/check-pr.yml: -------------------------------------------------------------------------------- 1 | name: "Check Pull Request" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | permissions: 11 | pull-requests: write 12 | 13 | jobs: 14 | main: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: amannn/action-semantic-pull-request@335288255954904a41ddda8947c8f2c844b8bfeb 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | types: | 23 | chore 24 | ci 25 | docs 26 | feat 27 | fix 28 | test 29 | sec 30 | requireScope: false 31 | wip: false 32 | # If the PR only contains a single commit, the action will validate that 33 | # it matches the configured pattern. 34 | validateSingleCommit: true 35 | # Related to `validateSingleCommit` you can opt-in to validate that the PR 36 | # title matches a single commit to avoid confusion. 37 | validateSingleCommitMatchesPrTitle: true 38 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | types: [opened, reopened, synchronize] 9 | branches: 10 | - "main" 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | compliance: 18 | name: "License Compliance" 19 | runs-on: ubuntu-24.04 20 | steps: 21 | - name: "Checkout Code" 22 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | - name: Check secret 24 | id: checksecret 25 | uses: ./.github/actions/exists 26 | with: 27 | value: ${{ secrets.FOSSA_API_KEY }} 28 | - name: "Run FOSSA Scan" 29 | if: steps.checksecret.outputs.result == 'true' 30 | uses: fossas/fossa-action@3ebcea1862c6ffbd5cf1b4d0bd6b3fe7bd6f2cac # v1.7.0 31 | with: 32 | api-key: ${{ secrets.FOSSA_API_KEY }} 33 | - name: "Run FOSSA Test" 34 | if: steps.checksecret.outputs.result == 'true' 35 | uses: fossas/fossa-action@3ebcea1862c6ffbd5cf1b4d0bd6b3fe7bd6f2cac # v1.7.0 36 | with: 37 | api-key: ${{ secrets.FOSSA_API_KEY }} 38 | run-tests: true 39 | sast: 40 | name: "SAST" 41 | runs-on: ubuntu-24.04 42 | env: 43 | GO111MODULE: on 44 | permissions: 45 | security-events: write 46 | actions: read 47 | contents: read 48 | steps: 49 | - name: Checkout Source 50 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 51 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 52 | with: 53 | go-version-file: 'go.mod' 54 | - name: Run Gosec Security Scanner 55 | uses: securego/gosec@6decf96c3d272d5a8bbdcf9fddb5789d0be16a8d # v2.22.4 56 | with: 57 | args: '-no-fail -fmt sarif -out gosec.sarif ./...' 58 | - name: Upload SARIF file 59 | uses: github/codeql-action/upload-sarif@7fd62151d9daff11d4b981415ffb365dcd93f75a 60 | with: 61 | sarif_file: gosec.sarif 62 | unit_tests: 63 | name: "Unit tests" 64 | runs-on: ubuntu-24.04 65 | steps: 66 | - name: Checkout 67 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 68 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 69 | with: 70 | go-version-file: 'go.mod' 71 | - name: Unit Test 72 | run: make test 73 | - name: Check secret 74 | id: checksecret 75 | uses: ./.github/actions/exists 76 | with: 77 | value: ${{ secrets.CODECOV_TOKEN }} 78 | - name: Upload Report to Codecov 79 | if: ${{ steps.checksecret.outputs.result == 'true' }} 80 | uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 81 | with: 82 | token: ${{ secrets.CODECOV_TOKEN }} 83 | slug: projectcapsule/capsule-proxy 84 | files: ./coverage.out 85 | fail_ci_if_error: true 86 | verbose: true 87 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Build images 2 | permissions: {} 3 | on: 4 | pull_request: 5 | branches: 6 | - "main" 7 | paths: 8 | - '.github/workflows/docker-*.yml' 9 | - 'api/**' 10 | - 'controllers/**' 11 | - 'pkg/**' 12 | - 'e2e/*' 13 | - '.ko.yaml' 14 | - 'go.*' 15 | - 'main.go' 16 | - 'Makefile' 17 | 18 | jobs: 19 | build-images: 20 | runs-on: ubuntu-latest 21 | permissions: 22 | security-events: write 23 | actions: read 24 | contents: read 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | - name: ko build 29 | run: VERSION=${{ github.sha }} make ko-build-all 30 | - name: Trivy Scan Image 31 | uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5 # 0.30.0 32 | with: 33 | scan-type: 'fs' 34 | ignore-unfixed: true 35 | format: 'sarif' 36 | output: 'trivy-results.sarif' 37 | severity: 'CRITICAL,HIGH' 38 | env: 39 | # Trivy is returning TOOMANYREQUESTS 40 | # See: https://github.com/aquasecurity/trivy-action/issues/389#issuecomment-2385416577 41 | TRIVY_DB_REPOSITORY: 'public.ecr.aws/aquasecurity/trivy-db:2' 42 | - name: Upload Trivy scan results to GitHub Security tab 43 | uses: github/codeql-action/upload-sarif@7fd62151d9daff11d4b981415ffb365dcd93f75a 44 | with: 45 | sarif_file: 'trivy-results.sarif' 46 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish images 2 | permissions: {} 3 | 4 | on: 5 | push: 6 | tags: 7 | - "v*" 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | publish-images: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | packages: write 18 | id-token: write 19 | outputs: 20 | capsule-digest: ${{ steps.publish-capsule.outputs.digest }} 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 24 | - name: Setup caches 25 | uses: ./.github/actions/setup-caches 26 | timeout-minutes: 5 27 | continue-on-error: true 28 | with: 29 | build-cache-key: publish-images 30 | - name: Run Trivy vulnerability (Repo) 31 | uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5 # 0.30.0 32 | with: 33 | scan-type: 'fs' 34 | ignore-unfixed: true 35 | format: 'sarif' 36 | output: 'trivy-results.sarif' 37 | severity: 'CRITICAL,HIGH' 38 | - name: Install Cosign 39 | uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 40 | - name: Publish Capsule 41 | id: publish-capsule 42 | uses: peak-scale/github-actions/make-ko-publish@a441cca016861c546ab7e065277e40ce41a3eb84 # v0.2.0 43 | with: 44 | makefile-target: ko-publish-capsule-proxy 45 | registry: ghcr.io 46 | registry-username: ${{ github.actor }} 47 | registry-password: ${{ secrets.GITHUB_TOKEN }} 48 | repository: ${{ github.repository_owner }} 49 | version: ${{ github.ref_name }} 50 | sign-image: true 51 | sbom-name: capsule-proxy 52 | sbom-repository: ghcr.io/${{ github.repository_owner }}/capsule-proxy 53 | signature-repository: ghcr.io/${{ github.repository_owner }}/capsule-proxy 54 | main-path: ./ 55 | env: 56 | REPOSITORY: ${{ github.repository }} 57 | generate-capsule-provenance: 58 | needs: publish-images 59 | permissions: 60 | id-token: write # To sign the provenance. 61 | packages: write # To upload assets to release. 62 | actions: read # To read the workflow path. 63 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 64 | with: 65 | image: ghcr.io/${{ github.repository_owner }}/capsule-proxy 66 | digest: "${{ needs.publish-images.outputs.capsule-digest }}" 67 | registry-username: ${{ github.actor }} 68 | secrets: 69 | registry-password: ${{ secrets.GITHUB_TOKEN }} 70 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: e2e 2 | permissions: {} 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - "*" 8 | paths: 9 | - '.github/workflows/e2e.yml' 10 | - 'api/**' 11 | - 'controllers/**' 12 | - 'internal/**' 13 | - 'e2e/*' 14 | - 'Dockerfile' 15 | - 'go.*' 16 | - 'main.go' 17 | - 'Makefile' 18 | 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.ref }} 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | kind: 25 | name: Kubernetes 26 | runs-on: ubuntu-24.04 27 | steps: 28 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 29 | with: 30 | fetch-depth: 0 31 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 32 | with: 33 | go-version-file: 'go.mod' 34 | - uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4 35 | with: 36 | version: v3.14.2 37 | - name: e2e testing 38 | run: make e2e 39 | -------------------------------------------------------------------------------- /.github/workflows/helm-test.yml: -------------------------------------------------------------------------------- 1 | name: Test charts 2 | permissions: {} 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - "main" 8 | paths: 9 | - '.github/configs/**' 10 | - '.github/workflows/helm-*.yml' 11 | - 'charts/**' 12 | - 'Makefile' 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | linter-artifacthub: 20 | runs-on: ubuntu-latest 21 | container: 22 | image: artifacthub/ah 23 | options: --user root 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 27 | - name: Run ah lint 28 | working-directory: ./charts/ 29 | run: ah lint 30 | lint: 31 | runs-on: ubuntu-24.04 32 | steps: 33 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 34 | with: 35 | fetch-depth: 0 36 | - uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4 37 | - name: Run chart-testing (lint) 38 | run: make helm-lint 39 | - name: Run docs-testing (helm-docs) 40 | id: helm-docs 41 | run: | 42 | make helm-docs 43 | if [[ $(git diff --stat) != '' ]]; then 44 | echo -e '\033[0;31mDocumentation outdated! (Run make helm-docs locally and commit)\033[0m ❌' 45 | git diff --color 46 | exit 1 47 | else 48 | echo -e '\033[0;32mDocumentation up to date\033[0m ✔' 49 | fi 50 | - name: Run schema-testing (helm-schema) 51 | id: helm-schema 52 | run: | 53 | make helm-schema 54 | if [[ $(git diff --stat) != '' ]]; then 55 | echo -e '\033[0;31mSchema outdated! (Run make helm-schema locally and commit)\033[0m ❌' 56 | git diff --color 57 | exit 1 58 | else 59 | echo -e '\033[0;32mSchema up to date\033[0m ✔' 60 | fi 61 | - name: Run chart-testing (install) 62 | run: make helm-test 63 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | permissions: {} 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | branches: 9 | - "*" 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | jobs: 14 | manifests: 15 | name: diff 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | with: 20 | fetch-depth: 0 21 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 22 | with: 23 | go-version-file: 'go.mod' 24 | - name: Generate manifests 25 | run: | 26 | make manifests 27 | if [[ $(git diff --stat) != '' ]]; then 28 | echo -e '\033[0;31mManifests outdated! (Run make manifests locally and commit)\033[0m ❌' 29 | git diff --color 30 | exit 1 31 | else 32 | echo -e '\033[0;32mDocumentation up to date\033[0m ✔' 33 | fi 34 | yamllint: 35 | name: yamllint 36 | runs-on: ubuntu-24.04 37 | steps: 38 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 39 | - name: Install yamllint 40 | run: pip install yamllint 41 | - name: Lint YAML files 42 | run: yamllint -c=.github/configs/lintconf.yaml . 43 | golangci: 44 | name: lint 45 | runs-on: ubuntu-24.04 46 | steps: 47 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 48 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 49 | with: 50 | go-version-file: 'go.mod' 51 | - name: Run golangci-lint 52 | run: make golint 53 | -------------------------------------------------------------------------------- /.github/workflows/releaser.yml: -------------------------------------------------------------------------------- 1 | name: Go Release 2 | 3 | permissions: {} 4 | on: 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | create-release: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | id-token: write 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | fetch-depth: 0 24 | - name: Setup caches 25 | uses: ./.github/actions/setup-caches 26 | timeout-minutes: 5 27 | continue-on-error: true 28 | - uses: creekorful/goreportcard-action@1f35ced8cdac2cba28c9a2f2288a16aacfd507f9 # v1.0 29 | - uses: anchore/sbom-action/download-syft@e11c554f704a0b820cbf8c51673f6945e0731532 30 | - name: Install Cosign 31 | uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 32 | - name: Run GoReleaser 33 | uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 34 | with: 35 | version: latest 36 | args: release --clean --timeout 90m 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | name: Scorecards supply-chain security 2 | permissions: {} 3 | 4 | on: 5 | schedule: 6 | - cron: '0 0 * * 5' 7 | push: 8 | branches: 9 | - main 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | analysis: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | security-events: write 20 | id-token: write 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 24 | with: 25 | persist-credentials: false 26 | - name: Run analysis 27 | uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 28 | with: 29 | results_file: results.sarif 30 | results_format: sarif 31 | repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} 32 | publish_results: true 33 | - name: Upload artifact 34 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 35 | with: 36 | name: SARIF file 37 | path: results.sarif 38 | retention-days: 5 39 | - name: Upload to code-scanning 40 | uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 41 | with: 42 | sarif_file: results.sarif 43 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Stale-Bot 2 | permissions: {} 3 | 4 | on: 5 | schedule: 6 | - cron: '0 0 * * *' # Run every day at midnight 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | actions: write 13 | contents: write # only for delete-branch option 14 | issues: write 15 | pull-requests: write 16 | steps: 17 | - name: Close stale pull requests 18 | uses: actions/stale@f78de9780efb7a789cf4745957fa3374cbb94fd5 19 | with: 20 | stale-issue-message: 'This pull request has been automatically closed because it has been inactive for more than 60 days. Please reopen if you still intend to submit this pull request.' 21 | days-before-stale: 60 22 | days-before-close: 30 23 | days-before-pr-stale: 30 24 | stale-pr-message: 'This pull request has been marked as stale because it has been inactive for more than 30 days. Please update this pull request or it will be automatically closed in 7 days.' 25 | stale-pr-label: stale 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Kubernetes Generated files - skip generated files, except for vendored files 17 | 18 | !vendor/**/zz_generated.* 19 | 20 | # editor and IDE paraphernalia 21 | .idea 22 | *.swp 23 | *.swo 24 | *~ 25 | .vscode 26 | .DS_Store 27 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | dupl: 3 | threshold: 100 4 | goconst: 5 | min-len: 2 6 | min-occurrences: 2 7 | cyclop: 8 | max-complexity: 27 9 | gocognit: 10 | min-complexity: 50 11 | gci: 12 | sections: 13 | - standard 14 | - default 15 | - prefix(github.com/projectcapsule/capsule-proxy) 16 | gofumpt: 17 | module-path: github.com/projectcapsule/capsule-proxy 18 | extra-rules: false 19 | inamedparam: 20 | # Skips check for interface methods with only a single parameter. 21 | # Default: false 22 | skip-single-param: true 23 | nakedret: 24 | # Make an issue if func has more lines of code than this setting, and it has naked returns. 25 | max-func-lines: 50 26 | linters: 27 | enable-all: true 28 | disable: 29 | - err113 30 | - depguard 31 | - perfsprint 32 | - funlen 33 | - gochecknoinits 34 | - lll 35 | - gochecknoglobals 36 | - mnd 37 | - nilnil 38 | - recvcheck 39 | - unparam 40 | - paralleltest 41 | - ireturn 42 | - testpackage 43 | - varnamelen 44 | - wrapcheck 45 | - exhaustruct 46 | - nonamedreturns 47 | issues: 48 | exclude-files: 49 | - "zz_.*\\.go$" 50 | - ".+\\.generated.go" 51 | - ".+_test.go" 52 | - ".+_test_.+.go" 53 | run: 54 | timeout: 3m 55 | allow-parallel-runners: true 56 | tests: false 57 | -------------------------------------------------------------------------------- /.ko.yaml: -------------------------------------------------------------------------------- 1 | defaultPlatforms: 2 | - linux/arm64 3 | - linux/amd64 4 | - linux/arm 5 | builds: 6 | - id: capsule-proxy 7 | main: ./ 8 | ldflags: 9 | - '{{ if index .Env "LD_FLAGS" }}{{ .Env.LD_FLAGS }}{{ end }}' 10 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook 3 | rev: v9.22.0 4 | hooks: 5 | - id: commitlint 6 | stages: [commit-msg] 7 | additional_dependencies: ['@commitlint/config-conventional', 'commitlint-plugin-function-rules'] 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: v5.0.0 10 | hooks: 11 | - id: check-executables-have-shebangs 12 | - id: double-quote-string-fixer 13 | - id: end-of-file-fixer 14 | - id: trailing-whitespace 15 | - repo: https://github.com/adrienverge/yamllint 16 | rev: v1.37.1 17 | hooks: 18 | - id: yamllint 19 | args: [-c=.github/configs/lintconf.yaml] 20 | - repo: local 21 | hooks: 22 | - id: run-helm-docs 23 | name: Execute helm-docs 24 | entry: make helm-docs 25 | language: system 26 | files: ^charts/ 27 | - id: run-helm-schema 28 | name: Execute helm-schema 29 | entry: make helm-schema 30 | language: system 31 | files: ^charts/ 32 | - id: run-helm-lint 33 | name: Execute helm-lint 34 | entry: make helm-lint 35 | language: system 36 | files: ^charts/ 37 | - id: golangci-lint 38 | name: Execute golangci-lint 39 | entry: make golint 40 | language: system 41 | files: \.go$ 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 as builder 2 | WORKDIR /workspace 3 | COPY go.mod go.mod 4 | COPY go.sum go.sum 5 | RUN go mod download 6 | COPY main.go main.go 7 | COPY internal internal 8 | COPY api api 9 | ARG GCFLAGS="" 10 | ARG TARGETARCH 11 | ARG LDFLAGS="" 12 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} GO111MODULE=on go build -trimpath -ldflags="${LDFLAGS}" -gcflags "${GCFLAGS}" -a -o capsule-proxy main.go 13 | 14 | FROM golang:1.24 as dlv 15 | RUN CGO_ENABLED=0 go install github.com/go-delve/delve/cmd/dlv@latest 16 | WORKDIR / 17 | COPY --from=builder /workspace/capsule-proxy . 18 | ENTRYPOINT ["dlv", "--listen=:2345", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "--", "/capsule-proxy"] 19 | 20 | FROM gcr.io/distroless/static:nonroot 21 | WORKDIR / 22 | COPY --from=builder /workspace/capsule-proxy . 23 | USER nonroot:nonroot 24 | ENTRYPOINT ["/capsule-proxy"] 25 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: clastix.io 6 | layout: 7 | - go.kubebuilder.io/v3 8 | plugins: 9 | manifests.sdk.operatorframework.io/v2: {} 10 | scorecard.sdk.operatorframework.io/v2: {} 11 | projectName: capsule-proxy 12 | repo: github.com/projectcapsule/capsule-proxy 13 | resources: 14 | - api: 15 | crdVersion: v1 16 | namespaced: true 17 | controller: true 18 | domain: clastix.io 19 | group: capsule 20 | kind: ProxySettings 21 | path: github.com/projectcapsule/capsule-proxy/api/v1beta1 22 | version: v1beta1 23 | - api: 24 | crdVersion: v1 25 | domain: clastix.io 26 | group: capsule 27 | kind: GlobalProxySettings 28 | path: github.com/projectcapsule/capsule-proxy/api/v1beta1 29 | version: v1beta1 30 | version: "3" 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Capsule Proxy 2 | 3 | This project is an add-on for [Capsule](https://github.com/projectcapsule/capsule), the operator providing multi-tenancy in Kubernetes. 4 | 5 | `capsule-proxy` allows to overcome the limitations of Kubernetes API Server on listing owned cluster-scoped resources, like Namespace, Ingress and Storage Classes, Nodes, and others covered by Capsule. 6 | 7 | ## Documentation 8 | 9 | You can find more detailed documentation [here](https://capsule.clastix.io/docs/general/proxy). 10 | 11 | ## Maintainers 12 | 13 | Please, refer to the maintainers file available [here](.github/maintainers.yaml). 14 | 15 | ## Contributions 16 | 17 | This is an open-source software released with Apache2 [license](./LICENSE). Feel free to open issues and pull requests. You're welcome! 18 | 19 | Contributing is available in the related [guide](./CONTRIBUTING.md). 20 | -------------------------------------------------------------------------------- /api/v1beta1/clusterresoure.go: -------------------------------------------------------------------------------- 1 | package v1beta1 2 | 3 | import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 4 | 5 | // +kubebuilder:validation:Enum=List;Update;Delete 6 | type ClusterResourceOperation string 7 | 8 | func (p ClusterResourceOperation) String() string { 9 | return string(p) 10 | } 11 | 12 | const ( 13 | ClusterResourceOperationList ClusterResourceOperation = "List" 14 | ) 15 | 16 | // +kubebuilder:object:generate=true 17 | type ClusterResource struct { 18 | // APIGroups is the name of the APIGroup that contains the resources. If multiple API groups are specified, any action requested against any resource listed will be allowed. '*' represents all resources. Empty string represents v1 api resources. 19 | APIGroups []string `json:"apiGroups"` 20 | 21 | // Resources is a list of resources this rule applies to. '*' represents all resources. 22 | Resources []string `json:"resources"` 23 | 24 | // Operations which can be executed on the selected resources. 25 | // Deprecated: For all registered Routes only LIST ang GET requests will intercepted 26 | // Other permissions must be implemented via kubernetes native RBAC 27 | Operations []ClusterResourceOperation `json:"operations,omitempty"` 28 | 29 | // Select all cluster scoped resources with the given label selector. 30 | // Defining a selector which does not match any resources is considered not selectable (eg. using operation NotExists). 31 | Selector *metav1.LabelSelector `json:"selector"` 32 | } 33 | -------------------------------------------------------------------------------- /api/v1beta1/globalproxysettings_types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package v1beta1 5 | 6 | import ( 7 | "github.com/projectcapsule/capsule/api/v1beta2" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | // GlobalProxySettingsSpec defines the desired state of GlobalProxySettings. 12 | type GlobalProxySettingsSpec struct { 13 | // Subjects that should receive additional permissions. 14 | // The subjects are selected based on the oncoming requests. They don't have to relate to an existing tenant. 15 | // However they must be part of the capsule-user groups. 16 | // +kubebuilder:validation:MinItems=1 17 | Rules []GlobalSubjectSpec `json:"rules"` 18 | } 19 | 20 | type GlobalSubjectSpec struct { 21 | // Subjects that should receive additional permissions. 22 | // The subjects are selected based on the oncoming requests. They don't have to relate to an existing tenant. 23 | // However they must be part of the capsule-user groups. 24 | Subjects []GlobalSubject `json:"subjects"` 25 | // Cluster Resources for tenant Owner. 26 | ClusterResources []ClusterResource `json:"clusterResources,omitempty"` 27 | } 28 | 29 | type GlobalSubject struct { 30 | // Kind of tenant owner. Possible values are "User", "Group", and "ServiceAccount". 31 | Kind v1beta2.OwnerKind `json:"kind"` 32 | // Name of tenant owner. 33 | Name string `json:"name"` 34 | } 35 | 36 | //+kubebuilder:object:root=true 37 | //+kubebuilder:subresource:status 38 | //+kubebuilder:resource:scope=Cluster 39 | 40 | // GlobalProxySettings is the Schema for the globalproxysettings API. 41 | type GlobalProxySettings struct { 42 | metav1.TypeMeta `json:",inline"` 43 | metav1.ObjectMeta `json:"metadata,omitempty"` 44 | 45 | Spec GlobalProxySettingsSpec `json:"spec,omitempty"` 46 | } 47 | 48 | //+kubebuilder:object:root=true 49 | 50 | // GlobalProxySettingsList contains a list of GlobalProxySettings. 51 | type GlobalProxySettingsList struct { 52 | metav1.TypeMeta `json:",inline"` 53 | metav1.ListMeta `json:"metadata,omitempty"` 54 | Items []GlobalProxySettings `json:"items"` 55 | } 56 | 57 | //nolint:gochecknoinits 58 | func init() { 59 | SchemeBuilder.Register(&GlobalProxySettings{}, &GlobalProxySettingsList{}) 60 | } 61 | -------------------------------------------------------------------------------- /api/v1beta1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package v1beta1 contains API Schema definitions for the capsule.clastix.io v1beta1 API group 5 | // +kubebuilder:object:generate=true 6 | // +groupName=capsule.clastix.io 7 | package v1beta1 8 | 9 | import ( 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "sigs.k8s.io/controller-runtime/pkg/scheme" 12 | ) 13 | 14 | var ( 15 | // GroupVersion is group version used to register these objects. 16 | //nolint:gochecknoglobals 17 | GroupVersion = schema.GroupVersion{Group: "capsule.clastix.io", Version: "v1beta1"} 18 | 19 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme. 20 | //nolint:gochecknoglobals 21 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 22 | 23 | // AddToScheme adds the types in this group-version to the given scheme. 24 | //nolint:gochecknoglobals 25 | AddToScheme = SchemeBuilder.AddToScheme 26 | ) 27 | -------------------------------------------------------------------------------- /api/v1beta1/proxysettings_types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package v1beta1 5 | 6 | import ( 7 | "github.com/projectcapsule/capsule/api/v1beta2" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | type OwnerSpec struct { 12 | // Kind of tenant owner. Possible values are "User", "Group", and "ServiceAccount" 13 | Kind v1beta2.OwnerKind `json:"kind"` 14 | // Name of tenant owner. 15 | Name string `json:"name"` 16 | // Proxy settings for tenant owner. 17 | ProxyOperations []v1beta2.ProxySettings `json:"proxySettings,omitempty"` 18 | // Cluster Resources for tenant Owner. 19 | ClusterResources []ClusterResource `json:"clusterResources,omitempty"` 20 | } 21 | 22 | // ProxySettingSpec defines the additional Capsule Proxy settings for additional users of the Tenant. 23 | // Resource is Namespace-scoped and applies the settings to the belonged Tenant. 24 | type ProxySettingSpec struct { 25 | // Subjects that should receive additional permissions. 26 | // +kubebuilder:validation:MinItems=1 27 | Subjects []OwnerSpec `json:"subjects"` 28 | } 29 | 30 | //+kubebuilder:object:root=true 31 | 32 | // ProxySetting is the Schema for the proxysettings API. 33 | type ProxySetting struct { 34 | metav1.TypeMeta `json:",inline"` 35 | metav1.ObjectMeta `json:"metadata,omitempty"` 36 | 37 | Spec ProxySettingSpec `json:"spec,omitempty"` 38 | } 39 | 40 | //+kubebuilder:object:root=true 41 | 42 | // ProxySettingList contains a list of ProxySetting. 43 | type ProxySettingList struct { 44 | metav1.TypeMeta `json:",inline"` 45 | metav1.ListMeta `json:"metadata,omitempty"` 46 | Items []ProxySetting `json:"items"` 47 | } 48 | 49 | //nolint:gochecknoinits 50 | func init() { 51 | SchemeBuilder.Register(&ProxySetting{}, &ProxySettingList{}) 52 | } 53 | -------------------------------------------------------------------------------- /charts/capsule-proxy/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | # Helm 25 | /*.tgz 26 | ci/ 27 | *.gotmpl 28 | .schema.yaml 29 | artifacthub-repo.yml 30 | -------------------------------------------------------------------------------- /charts/capsule-proxy/.schema.yaml: -------------------------------------------------------------------------------- 1 | input: 2 | - ci/cert-manager-values.yaml 3 | - ci/backwards-values.yaml 4 | - ci/deploy-values.yaml 5 | - ci/ds-values.yaml 6 | - ci/webhook-values.yaml 7 | -------------------------------------------------------------------------------- /charts/capsule-proxy/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | appVersion: 0.0.0 3 | description: Helm Chart for Capsule Proxy, addon for Capsule, the multi-tenant Operator 4 | name: capsule-proxy 5 | type: application 6 | version: 0.0.0 7 | home: https://github.com/projectcapsule/capsule-proxy 8 | icon: https://github.com/projectcapsule/capsule/raw/main/assets/logo/capsule_small.png 9 | keywords: 10 | - kubernetes 11 | - operator 12 | - multi-tenancy 13 | - multi-tenant 14 | - multitenancy 15 | - multitenant 16 | - namespace 17 | - proxy 18 | sources: 19 | - https://projectcapsule.dev/integrations/addons/capsule-proxy/ 20 | maintainers: 21 | - name: capsule-maintainers 22 | email: cncf-capsule-maintainers@lists.cncf.io 23 | annotations: 24 | artifacthub.io/containsSecurityUpdates: "false" 25 | artifacthub.io/operator: "true" 26 | artifacthub.io/prerelease: "false" 27 | artifacthub.io/category: security 28 | artifacthub.io/license: Apache-2.0 29 | artifacthub.io/maintainers: | 30 | - name: capsule-maintainers 31 | email: cncf-capsule-maintainers@lists.cncf.io 32 | artifacthub.io/links: | 33 | - name: Documentation 34 | url: https://projectcapsule.dev/ 35 | artifacthub.io/changes: | 36 | - kind: changed 37 | description: cert-job image from docker.io/jettech/kube-webhook-certgen to registry.k8s.io/ingress-nginx/kube-webhook-certgen (.Values.global.jobs.certs.image) 38 | - kind: added 39 | description: Webhook values 40 | -------------------------------------------------------------------------------- /charts/capsule-proxy/artifacthub-repo.yml: -------------------------------------------------------------------------------- 1 | repositoryID: 6f8cec74-cd77-4792-a0e7-675147fe20c3 2 | owners: 3 | - name: capsule-maintainers 4 | email: cncf-capsule-maintainers@lists.cncf.io 5 | -------------------------------------------------------------------------------- /charts/capsule-proxy/ci/backwards-values.yaml: -------------------------------------------------------------------------------- 1 | crds: 2 | install: true 3 | keep: false 4 | global: 5 | jobs: 6 | certs: 7 | resources: 8 | limits: 9 | memory: 128Mi 10 | cpu: 0.125 11 | jobs: 12 | resources: 13 | requests: 14 | memory: 128Mi 15 | cpu: 0.125 16 | -------------------------------------------------------------------------------- /charts/capsule-proxy/ci/cert-manager-values.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | jobs: 3 | kubectl: 4 | ttlSecondsAfterFinished: 120 5 | crds: 6 | install: true 7 | keep: false 8 | options: 9 | enableSSL: true 10 | generateCertificates: false 11 | certManager: 12 | generateCertificates: true 13 | certificate: 14 | dnsNames: 15 | - "localhost" 16 | ipAddresses: 17 | - "127.0.0.1" 18 | uris: 19 | - "spiffe://cluster.local/ns/sandbox/sa/example" 20 | fields: 21 | privateKey: 22 | rotationPolicy: 'Always' 23 | renewBefore: '24h' 24 | -------------------------------------------------------------------------------- /charts/capsule-proxy/ci/deploy-values.yaml: -------------------------------------------------------------------------------- 1 | crds: 2 | install: true 3 | keep: false 4 | rbac: 5 | annotations: 6 | extra: annotation 7 | labels: 8 | extra: label 9 | kind: DaemonSet 10 | imagePullSecrets: [] 11 | certManager: 12 | generateCertificates: true 13 | externalCA: 14 | enabled: false 15 | # secret containing the CA cert and private key of the external CA in the correct cert-manager format as per https://cert-manager.io/docs/configuration/ca/#deployment 16 | secretName: "" 17 | issuer: 18 | kind: Issuer # Issuer or ClusterIssuer 19 | name: "" # Name of the ClusterIssuer 20 | replicaCount: 1 21 | podAnnotations: 22 | scheduler.alpha.kubernetes.io/critical-pod: '' 23 | extra: annotation 24 | podLabels: 25 | extra: label 26 | topologySpreadConstraints: 27 | - maxSkew: 1 28 | topologyKey: kubernetes.io/hostname 29 | whenUnsatisfiable: ScheduleAnyway 30 | labelSelector: 31 | matchLabels: 32 | app.kubernetes.io/name: capsule-proxy 33 | priorityClassName: "system-node-critical" 34 | resources: 35 | limits: 36 | cpu: 200m 37 | memory: 128Mi 38 | requests: 39 | cpu: 200m 40 | memory: 128Mi 41 | autoscaling: 42 | enabled: true 43 | annotations: 44 | example: annotation 45 | labels: 46 | example: label 47 | minReplicas: 1 48 | maxReplicas: 5 49 | targetCPUUtilizationPercentage: 80 50 | metrics: 51 | - type: Pods 52 | pods: 53 | metric: 54 | name: packets-per-second 55 | target: 56 | type: AverageValue 57 | averageValue: 1k 58 | behavior: 59 | scaleDown: 60 | policies: 61 | - type: Pods 62 | value: 4 63 | periodSeconds: 60 64 | - type: Percent 65 | value: 10 66 | periodSeconds: 60 67 | nodeSelector: 68 | node-role.kubernetes.io/master: "" 69 | tolerations: 70 | - key: CriticalAddonsOnly 71 | operator: Exists 72 | - effect: NoSchedule 73 | key: node-role.kubernetes.io/master 74 | service: 75 | annotations: 76 | example: annotation 77 | labels: 78 | example: label 79 | # Ingress 80 | ingress: 81 | enabled: true 82 | ingressClassName: "nginx" 83 | annotations: 84 | example: annotation 85 | labels: 86 | example: label 87 | hosts: 88 | - host: "kube.clastix.io" 89 | paths: ["/"] 90 | tls: 91 | - hosts: 92 | - kube.clastix.io 93 | secretName: capsule-proxy-tls 94 | # ServiceMonitor 95 | serviceMonitor: 96 | enabled: true 97 | -------------------------------------------------------------------------------- /charts/capsule-proxy/ci/ds-values.yaml: -------------------------------------------------------------------------------- 1 | crds: 2 | install: true 3 | keep: false 4 | kind: DaemonSet 5 | daemonset: 6 | hostNetwork: true 7 | hostPort: true 8 | imagePullSecrets: [] 9 | certManager: 10 | generateCertificates: true 11 | externalCA: 12 | enabled: false 13 | # secret containing the CA cert and private key of the external CA in the correct cert-manager format as per https://cert-manager.io/docs/configuration/ca/#deployment 14 | secretName: "" 15 | issuer: 16 | kind: Issuer # Issuer or ClusterIssuer 17 | name: "" # Name of the ClusterIssuer 18 | replicaCount: 1 19 | podAnnotations: 20 | scheduler.alpha.kubernetes.io/critical-pod: '' 21 | extra: annotation 22 | podLabels: 23 | extra: label 24 | topologySpreadConstraints: 25 | - maxSkew: 1 26 | topologyKey: kubernetes.io/hostname 27 | whenUnsatisfiable: ScheduleAnyway 28 | labelSelector: 29 | matchLabels: 30 | app.kubernetes.io/name: capsule-proxy 31 | priorityClassName: "system-node-critical" 32 | resources: 33 | limits: 34 | cpu: 200m 35 | memory: 128Mi 36 | requests: 37 | cpu: 200m 38 | memory: 128Mi 39 | autoscaling: 40 | enabled: true 41 | minReplicas: 1 42 | maxReplicas: 5 43 | targetCPUUtilizationPercentage: 80 44 | nodeSelector: 45 | node-role.kubernetes.io/master: "" 46 | tolerations: 47 | - key: CriticalAddonsOnly 48 | operator: Exists 49 | - effect: NoSchedule 50 | key: node-role.kubernetes.io/master 51 | affinity: 52 | nodeAffinity: 53 | preferredDuringSchedulingIgnoredDuringExecution: 54 | - weight: 1 55 | preference: 56 | matchExpressions: 57 | - key: another-node-label-key 58 | operator: In 59 | values: 60 | - another-node-label-value 61 | # Ingress 62 | ingress: 63 | enabled: true 64 | ingressClassName: "nginx" 65 | hosts: 66 | - host: "kube.clastix.io" 67 | paths: ["/"] 68 | tls: 69 | - hosts: 70 | - kube.clastix.io 71 | secretName: capsule-proxy-tls 72 | # ServiceMonitor 73 | serviceMonitor: 74 | enabled: true 75 | jobs: 76 | affinity: 77 | nodeAffinity: 78 | preferredDuringSchedulingIgnoredDuringExecution: 79 | - weight: 1 80 | preference: 81 | matchExpressions: 82 | - key: another-node-label-key 83 | operator: In 84 | values: 85 | - another-node-label-value 86 | topologySpreadConstraints: 87 | - maxSkew: 1 88 | topologyKey: kubernetes.io/hostname 89 | whenUnsatisfiable: ScheduleAnyway 90 | labelSelector: 91 | matchLabels: 92 | app.kubernetes.io/name: capsule-proxy 93 | priorityClassName: "system-node-critical" 94 | nodeSelector: 95 | node-role.kubernetes.io/master: "" 96 | tolerations: 97 | - key: CriticalAddonsOnly 98 | operator: Exists 99 | - effect: NoSchedule 100 | key: node-role.kubernetes.io/master 101 | -------------------------------------------------------------------------------- /charts/capsule-proxy/ci/webhook-values.yaml: -------------------------------------------------------------------------------- 1 | webhooks: 2 | enabled: true 3 | -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | - 🚀 Capsule-proxy Helm Chart deployed: 2 | 3 | # Check the capsule-proxy logs 4 | $ kubectl logs -f deployment/{{ template "capsule-proxy.fullname" . }} -n {{ .Release.Namespace }} 5 | 6 | - 🛠️ Manage this chart: 7 | 8 | # Upgrade capsule-proxy 9 | $ helm upgrade {{ .Release.Name }} -f capsule-proxy -n {{ .Release.Namespace }} 10 | 11 | # Show this status again 12 | $ helm status {{ .Release.Name }} -n {{ .Release.Namespace }} 13 | 14 | # Uninstall capsule-proxy 15 | $ helm uninstall {{ .Release.Name }} -n {{ .Release.Namespace }} 16 | 17 | - 📚 Read More on the configuration for the capulse-proxy here: https://projectcapsule.dev/integrations/addons/capsule-proxy/ 18 | -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/_jobs.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Determine the Kubernetes version to use for jobsFullyQualifiedDockerImage tag 3 | */}} 4 | {{- define "capsule-proxy.jobsTagKubeVersion" -}} 5 | {{- if contains "-eks-" .Capabilities.KubeVersion.GitVersion }} 6 | {{- print "v" .Capabilities.KubeVersion.Major "." (.Capabilities.KubeVersion.Minor | replace "+" "") -}} 7 | {{- else }} 8 | {{- print "v" .Capabilities.KubeVersion.Major "." .Capabilities.KubeVersion.Minor -}} 9 | {{- end }} 10 | {{- end }} 11 | 12 | {{/* 13 | Create the jobs fully-qualified Docker image to use 14 | */}} 15 | {{- define "capsule-proxy.kubectlFullyQualifiedDockerImage" -}} 16 | {{- if .Values.global.jobs.kubectl.image.tag }} 17 | {{- printf "%s/%s:%s" .Values.global.jobs.kubectl.image.registry .Values.global.jobs.kubectl.image.repository .Values.global.jobs.kubectl.image.tag -}} 18 | {{- else }} 19 | {{- printf "%s/%s:%s" .Values.global.jobs.kubectl.image.registry .Values.global.jobs.kubectl.image.repository (include "capsule-proxy.jobsTagKubeVersion" .) -}} 20 | {{- end }} 21 | {{- end }} 22 | 23 | {{/* 24 | Create the certs jobs fully-qualified Docker image to use 25 | */}} 26 | {{- define "capsule.jobs.certsFullyQualifiedDockerImage" -}} 27 | {{- printf "%s/%s:%s" (default $.Values.global.jobs.certs.image.registry $.Values.jobs.certs.registry) (default $.Values.global.jobs.certs.image.repository $.Values.jobs.certs.repository) (default $.Values.global.jobs.certs.image.tag $.Values.jobs.certs.tag) -}} 28 | {{- end -}} 29 | -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/certgen-job.yaml: -------------------------------------------------------------------------------- 1 | {{/* Backwards compatibility */}} 2 | {{- $Values := mergeOverwrite $.Values.global.jobs.certs $.Values.jobs -}} 3 | 4 | {{- if and .Values.options.enableSSL .Values.options.generateCertificates -}} 5 | apiVersion: batch/v1 6 | kind: Job 7 | metadata: 8 | name: {{ include "capsule-proxy.fullname" . }}-certgen 9 | labels: 10 | {{- include "capsule-proxy.labels" . | nindent 4 }} 11 | {{- with $Values.annotations }} 12 | annotations: 13 | {{- toYaml . | nindent 4 }} 14 | {{- end }} 15 | spec: 16 | ttlSecondsAfterFinished: {{ $Values.ttlSecondsAfterFinished }} 17 | template: 18 | metadata: 19 | name: {{ include "capsule-proxy.fullname" . }}-certgen 20 | labels: 21 | {{- include "capsule-proxy.selectorLabels" . | nindent 8 }} 22 | spec: 23 | restartPolicy: {{ $Values.restartPolicy }} 24 | {{- with $Values.podSecurityContext }} 25 | securityContext: 26 | {{- toYaml . | nindent 8 }} 27 | {{- end }} 28 | {{- with $Values.nodeSelector }} 29 | nodeSelector: 30 | {{- toYaml . | nindent 8 }} 31 | {{- end }} 32 | {{- with $Values.affinity }} 33 | affinity: 34 | {{- toYaml . | nindent 8 }} 35 | {{- end }} 36 | {{- with $Values.tolerations }} 37 | tolerations: 38 | {{- toYaml . | nindent 8 }} 39 | {{- end }} 40 | {{- with $Values.topologySpreadConstraints }} 41 | topologySpreadConstraints: 42 | {{- toYaml . | nindent 8 }} 43 | {{- end }} 44 | {{- with $Values.priorityClassName }} 45 | priorityClassName: {{ . }} 46 | {{- end }} 47 | containers: 48 | - name: post-install-job 49 | image: {{ include "capsule.jobs.certsFullyQualifiedDockerImage" $ }} 50 | imagePullPolicy: {{ default $.Values.global.jobs.certs.image.pullPolicy $.Values.jobs.certs.pullPolicy }} 51 | args: 52 | - create 53 | - --host={{ include "capsule-proxy.certJob.SAN" . }} 54 | - --namespace=$(NAMESPACE) 55 | - --secret-name={{ include "capsule-proxy.fullname" . }} 56 | - --cert-name={{ .Values.options.SSLCertFileName }} 57 | - --key-name={{ .Values.options.SSLKeyFileName }} 58 | {{- with $Values.resources }} 59 | resources: 60 | {{- toYaml . | nindent 10 }} 61 | {{- end }} 62 | env: 63 | - name: NAMESPACE 64 | valueFrom: 65 | fieldRef: 66 | fieldPath: metadata.namespace 67 | {{- with $Values.securityContext }} 68 | securityContext: 69 | {{- toYaml . | nindent 10 }} 70 | {{- end }} 71 | {{- with $.Values.imagePullSecrets }} 72 | imagePullSecrets: 73 | {{- toYaml . | nindent 8 }} 74 | {{- end }} 75 | serviceAccountName: {{ include "capsule-proxy.serviceAccountName" . }} 76 | {{- end }} 77 | -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/certmanager.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.certManager.generateCertificates .Values.options.enableSSL -}} 2 | {{- if and (not .Values.certManager.externalCA.enabled) (eq .Values.certManager.issuer.kind "Issuer") -}} 3 | --- 4 | apiVersion: cert-manager.io/v1 5 | kind: Issuer 6 | metadata: 7 | name: {{ include "capsule-proxy.fullname" . }}-selfsigned-issuer 8 | spec: 9 | selfSigned: {} 10 | --- 11 | apiVersion: cert-manager.io/v1 12 | kind: Certificate 13 | metadata: 14 | name: {{ include "capsule-proxy.fullname" . }}-selfsigned-ca 15 | spec: 16 | isCA: true 17 | commonName: {{ include "capsule-proxy.fullname" . }}-selfsigned-ca 18 | secretName: {{ include "capsule-proxy.caSecretName" . }} 19 | privateKey: 20 | algorithm: ECDSA 21 | size: 256 22 | issuerRef: 23 | name: {{ include "capsule-proxy.fullname" . }}-selfsigned-issuer 24 | kind: Issuer 25 | group: cert-manager.io 26 | {{- end }} 27 | {{- if eq .Values.certManager.issuer.kind "Issuer" }} 28 | --- 29 | apiVersion: cert-manager.io/v1 30 | kind: Issuer 31 | metadata: 32 | name: {{ include "capsule-proxy.fullname" . }}-ca-issuer 33 | spec: 34 | ca: 35 | secretName: {{ include "capsule-proxy.caSecretName" . }} 36 | {{- end }} 37 | --- 38 | apiVersion: cert-manager.io/v1 39 | kind: Certificate 40 | metadata: 41 | name: {{ include "capsule-proxy.fullname" . }}-serving-cert 42 | spec: 43 | {{- with .Values.certManager.certificate.fields }} 44 | {{ toYaml . | nindent 2 }} 45 | {{- end }} 46 | dnsNames: 47 | {{- if .Values.ingress.enabled -}} 48 | {{- range $hosts := .Values.ingress.hosts }} 49 | - {{ $hosts.host | quote }} 50 | {{- end }} 51 | {{- end }} 52 | {{- range $dns := .Values.certManager.certificate.dnsNames }} 53 | - {{ $dns | quote }} 54 | {{- end }} 55 | {{- if $.Values.certManager.certificate.includeInternalServiceNames }} 56 | - {{ include "capsule-proxy.fullname" . }} 57 | - {{ include "capsule-proxy.fullname" . }}.{{ .Release.Namespace }}.svc 58 | {{- end }} 59 | {{- with .Values.certManager.certificate.ipAddresses }} 60 | ipAddresses: 61 | {{- range $ip := . }} 62 | - {{ $ip }} 63 | {{- end }} 64 | {{- end }} 65 | {{- with .Values.certManager.certificate.uris }} 66 | uris: 67 | {{- range $uri := . }} 68 | - {{ $uri }} 69 | {{- end }} 70 | {{- end }} 71 | issuerRef: 72 | kind: {{ .Values.certManager.issuer.kind }} 73 | name: {{ include "capsule-proxy.certManager.issuerName" . }} 74 | secretName: {{ include "capsule-proxy.fullname" . }} 75 | subject: 76 | organizations: 77 | - projectcapsule.dev 78 | {{- end }} -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/crd-lifecycle/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{- define "capsule-proxy.crds.name" -}} 2 | {{- printf "%s-crds" (include "capsule-proxy.name" $) -}} 3 | {{- end }} 4 | 5 | {{- define "capsule-proxy.crds.annotations" -}} 6 | "helm.sh/hook": "pre-install,pre-upgrade" 7 | "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" 8 | {{- with $.Values.global.jobs.annotations }} 9 | {{- . | toYaml | nindent 0 }} 10 | {{- end }} 11 | {{- end }} 12 | 13 | {{- define "capsule-proxy.crds.component" -}} 14 | crd-install-hook 15 | {{- end }} 16 | 17 | {{- define "capsule-proxy.crds.regexReplace" -}} 18 | {{- printf "%s" ($ | base | trimSuffix ".yaml" | regexReplaceAll "[_.]" "-") -}} 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/crd-lifecycle/crds.tpl: -------------------------------------------------------------------------------- 1 | {{/* CustomResources Lifecycle */}} 2 | {{- if $.Values.crds.install }} 3 | {{ range $path, $_ := .Files.Glob "crds/**.yaml" }} 4 | {{- with $ }} 5 | {{- $content := (tpl (.Files.Get $path) $) -}} 6 | {{- $p := (fromYaml $content) -}} 7 | {{- if $p.Error }} 8 | {{- fail (printf "found YAML error in file %s - %s - raw:\n\n%s" $path $p.Error $content) -}} 9 | {{- end -}} 10 | 11 | 12 | {{/* Add Common Lables */}} 13 | {{- $_ := set $p.metadata "labels" (mergeOverwrite (default dict (get $p.metadata "labels")) (default dict $.Values.crds.labels) (fromYaml (include "capsule-proxy.labels" $))) -}} 14 | 15 | 16 | {{/* Add Common Lables */}} 17 | {{- $_ := set $p.metadata "annotations" (mergeOverwrite (default dict (get $p.metadata "annotations")) (default dict $.Values.crds.annotations)) -}} 18 | 19 | {{/* Add Keep annotation to CRDs */}} 20 | {{- if $.Values.crds.keep }} 21 | {{- $_ := set $p.metadata.annotations "helm.sh/resource-policy" "keep" -}} 22 | {{- end }} 23 | 24 | {{/* Add Spec Patches for the CRD */}} 25 | {{- $patchFile := $path | replace ".yaml" ".patch" }} 26 | {{- $patchRawContent := (tpl (.Files.Get $patchFile) $) -}} 27 | {{- if $patchRawContent -}} 28 | {{- $patchContent := (fromYaml $patchRawContent) -}} 29 | {{- if $patchContent.Error }} 30 | {{- fail (printf "found YAML error in patch file %s - %s - raw:\n\n%s" $patchFile $patchContent.Error $patchRawContent) -}} 31 | {{- end -}} 32 | {{- $tmp := deepCopy $p | mergeOverwrite $patchContent -}} 33 | {{- $p = $tmp -}} 34 | {{- end -}} 35 | {{- if $p }} 36 | --- 37 | apiVersion: v1 38 | kind: ConfigMap 39 | metadata: 40 | name: {{ include "capsule-proxy.crds.name" . }}-{{ $path | base | trimSuffix ".yaml" | regexFind "[^_]+$" }} 41 | namespace: {{ .Release.Namespace | quote }} 42 | annotations: 43 | # create hook dependencies in the right order 44 | "helm.sh/hook-weight": "-5" 45 | {{- include "capsule-proxy.crds.annotations" . | nindent 4 }} 46 | labels: 47 | app.kubernetes.io/component: {{ include "capsule-proxy.crds.component" . | quote }} 48 | {{- include "capsule-proxy.labels" . | nindent 4 }} 49 | data: 50 | content: | 51 | {{- printf "---\n%s" (toYaml $p) | nindent 4 }} 52 | 53 | {{- end }} 54 | {{ end }} 55 | {{- end }} 56 | {{- end }} -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/crd-lifecycle/rbac.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.crds.install }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ include "capsule-proxy.crds.name" . }} 6 | namespace: {{ .Release.Namespace | quote }} 7 | annotations: 8 | # create hook dependencies in the right order 9 | "helm.sh/hook-weight": "-3" 10 | {{- include "capsule-proxy.crds.annotations" . | nindent 4 }} 11 | labels: 12 | app.kubernetes.io/component: {{ include "capsule-proxy.crds.component" . | quote }} 13 | {{- include "capsule-proxy.labels" . | nindent 4 }} 14 | rules: 15 | - apiGroups: 16 | - "" 17 | resources: 18 | - jobs 19 | verbs: 20 | - create 21 | - delete 22 | - apiGroups: 23 | - apiextensions.k8s.io 24 | resources: 25 | - customresourcedefinitions 26 | verbs: 27 | - create 28 | - delete 29 | - get 30 | - patch 31 | --- 32 | apiVersion: rbac.authorization.k8s.io/v1 33 | kind: ClusterRoleBinding 34 | metadata: 35 | name: {{ include "capsule-proxy.crds.name" . }} 36 | namespace: {{ .Release.Namespace | quote }} 37 | annotations: 38 | # create hook dependencies in the right order 39 | "helm.sh/hook-weight": "-2" 40 | {{- include "capsule-proxy.crds.annotations" . | nindent 4 }} 41 | labels: 42 | app.kubernetes.io/component: {{ include "capsule-proxy.crds.component" . | quote }} 43 | {{- include "capsule-proxy.labels" . | nindent 4 }} 44 | roleRef: 45 | apiGroup: rbac.authorization.k8s.io 46 | kind: ClusterRole 47 | name: {{ include "capsule-proxy.crds.name" . }} 48 | subjects: 49 | - kind: ServiceAccount 50 | name: {{ include "capsule-proxy.crds.name" . }} 51 | namespace: {{ .Release.Namespace | quote }} 52 | {{- end }} -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/crd-lifecycle/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.crds.install }} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "capsule-proxy.crds.name" . }} 6 | namespace: {{ .Release.Namespace }} 7 | annotations: 8 | # create hook dependencies in the right order 9 | "helm.sh/hook-weight": "-4" 10 | {{- include "capsule-proxy.crds.annotations" . | nindent 4 }} 11 | labels: 12 | app.kubernetes.io/component: {{ include "capsule-proxy.crds.component" . | quote }} 13 | {{- include "capsule-proxy.labels" . | nindent 4 }} 14 | {{- end }} -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/crds.tpl: -------------------------------------------------------------------------------- 1 | {{/* CustomResources Lifecycle */}} 2 | {{- if $.Values.crds.install }} 3 | {{ range $path, $_ := .Files.Glob "crd/**" }} 4 | {{- with $ }} 5 | {{- $content := (tpl (.Files.Get $path) .) -}} 6 | {{- $p := (fromYaml $content) -}} 7 | 8 | {{/* Add Common Lables */}} 9 | {{- $_ := set $p.metadata "labels" (mergeOverwrite (default dict (get $p.metadata "labels")) (fromYaml (include "capsule-proxy.labels" $))) -}} 10 | 11 | {{/* Add Keep annotation to CRDs */}} 12 | {{- if $.Values.crds.keep }} 13 | {{- $_ := set $p.metadata.annotations "helm.sh/resource-policy" "keep" -}} 14 | {{- end }} 15 | 16 | {{- if $p }} 17 | {{- printf "---\n%s" (toYaml $p) | nindent 0 }} 18 | {{- end }} 19 | {{ end }} 20 | {{- end }} 21 | {{- end }} -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/daemonset.yaml: -------------------------------------------------------------------------------- 1 | {{- if eq .Values.kind "DaemonSet" }} 2 | apiVersion: apps/v1 3 | kind: DaemonSet 4 | metadata: 5 | name: {{ include "capsule-proxy.fullname" . }} 6 | labels: 7 | {{- include "capsule-proxy.labels" . | nindent 4 }} 8 | spec: 9 | updateStrategy: 10 | type: RollingUpdate 11 | selector: 12 | matchLabels: 13 | {{- include "capsule-proxy.selectorLabels" . | nindent 6 }} 14 | template: 15 | {{- include "capsule-proxy.pod" $ | nindent 4 }} 16 | {{- end }} 17 | -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | {{- if eq .Values.kind "Deployment" }} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ include "capsule-proxy.fullname" . }} 6 | labels: 7 | {{- include "capsule-proxy.labels" . | nindent 4 }} 8 | spec: 9 | strategy: 10 | type: RollingUpdate 11 | {{- if not .Values.autoscaling.enabled }} 12 | replicas: {{ .Values.replicaCount }} 13 | {{- end }} 14 | selector: 15 | matchLabels: 16 | {{- include "capsule-proxy.selectorLabels" . | nindent 6 }} 17 | template: 18 | {{- include "capsule-proxy.pod" $ | nindent 4 }} 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | {{- if semverCompare "<1.23-0" $.Capabilities.KubeVersion.Version }} 3 | apiVersion: autoscaling/v2beta1 4 | {{- else }} 5 | apiVersion: autoscaling/v2 6 | {{- end }} 7 | kind: HorizontalPodAutoscaler 8 | metadata: 9 | name: {{ include "capsule-proxy.fullname" . }} 10 | labels: 11 | {{- include "capsule-proxy.labels" . | nindent 4 }} 12 | {{- if .Values.autoscaling.labels }} 13 | {{- toYaml .Values.autoscaling.labels | nindent 4 }} 14 | {{- end }} 15 | {{- with .Values.autoscaling.annotations }} 16 | annotations: 17 | {{- toYaml . | nindent 4 }} 18 | {{- end }} 19 | spec: 20 | scaleTargetRef: 21 | apiVersion: apps/v1 22 | kind: Deployment 23 | name: {{ include "capsule-proxy.fullname" . }} 24 | minReplicas: {{ .Values.autoscaling.minReplicas }} 25 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 26 | metrics: 27 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 28 | - type: Resource 29 | resource: 30 | name: cpu 31 | {{- if semverCompare "<1.23-0" $.Capabilities.KubeVersion.Version }} 32 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 33 | {{- else }} 34 | target: 35 | type: Utilization 36 | averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 37 | {{- end }} 38 | {{- end }} 39 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 40 | - type: Resource 41 | resource: 42 | name: memory 43 | {{- if semverCompare "<1.23-0" $.Capabilities.KubeVersion.Version }} 44 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 45 | {{- else }} 46 | target: 47 | type: Utilization 48 | averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 49 | {{- end }} 50 | {{- end }} 51 | {{- if .Values.autoscaling.metrics }} 52 | {{- toYaml .Values.autoscaling.metrics | nindent 4 }} 53 | {{- end }} 54 | {{- end }} 55 | -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "capsule-proxy.fullname" . -}} 3 | {{- if .Capabilities.APIVersions.Has "networking.k8s.io/v1/Ingress" -}} 4 | apiVersion: networking.k8s.io/v1 5 | {{- else -}} 6 | apiVersion: networking.k8s.io/v1beta1 7 | {{- end }} 8 | kind: Ingress 9 | metadata: 10 | name: {{ $fullName }} 11 | labels: 12 | {{- include "capsule-proxy.labels" . | nindent 4 }} 13 | {{- if .Values.ingress.labels }} 14 | {{- toYaml .Values.ingress.labels | nindent 4 }} 15 | {{- end }} 16 | {{- with .Values.ingress.annotations }} 17 | annotations: 18 | {{- toYaml . | nindent 4 }} 19 | {{- end }} 20 | spec: 21 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 22 | ingressClassName: {{ .Values.ingress.className }} 23 | {{- end }} 24 | {{- if .Values.ingress.tls }} 25 | tls: 26 | {{- range .Values.ingress.tls }} 27 | - hosts: 28 | {{- range .hosts }} 29 | - {{ . | quote }} 30 | {{- end }} 31 | secretName: {{ .secretName }} 32 | {{- end }} 33 | {{- end }} 34 | rules: 35 | {{- range .Values.ingress.hosts }} 36 | - host: {{ .host | quote }} 37 | http: 38 | paths: 39 | {{- range .paths }} 40 | {{- if $.Capabilities.APIVersions.Has "networking.k8s.io/v1/Ingress" }} 41 | - path: {{ . }} 42 | pathType: ImplementationSpecific 43 | backend: 44 | service: 45 | name: {{ $fullName }} 46 | port: 47 | number: {{ $.Values.service.port }} 48 | {{- else }} 49 | - path: {{ . }} 50 | backend: 51 | serviceName: {{ $fullName }} 52 | servicePort: {{ $.Values.service.port }} 53 | {{- end }} 54 | {{- end }} 55 | {{- end }} 56 | {{- end }} 57 | -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/metrics-rbac.yaml: -------------------------------------------------------------------------------- 1 | {{- if $.Values.rbac.enabled }} 2 | {{- if .Values.serviceMonitor.enabled }} 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: Role 5 | metadata: 6 | labels: 7 | {{- include "capsule-proxy.labels" . | nindent 4 }} 8 | {{- if .Values.serviceMonitor.labels }} 9 | {{- toYaml .Values.serviceMonitor.labels | nindent 4 }} 10 | {{- end }} 11 | {{- with .Values.customAnnotations }} 12 | annotations: 13 | {{- toYaml . | nindent 4 }} 14 | {{- end }} 15 | name: {{ include "capsule-proxy.fullname" . }}-metrics-role 16 | namespace: {{ .Values.serviceMonitor.namespace | default .Release.Namespace }} 17 | rules: 18 | - apiGroups: 19 | - "" 20 | resources: 21 | - services 22 | - endpoints 23 | - pods 24 | verbs: 25 | - get 26 | - list 27 | - watch 28 | --- 29 | apiVersion: rbac.authorization.k8s.io/v1 30 | kind: RoleBinding 31 | metadata: 32 | labels: 33 | {{- include "capsule-proxy.labels" . | nindent 4 }} 34 | {{- if .Values.serviceMonitor.labels }} 35 | {{- toYaml .Values.serviceMonitor.labels | nindent 4 }} 36 | {{- end }} 37 | name: {{ include "capsule-proxy.fullname" . }}-metrics-rolebinding 38 | namespace: {{ .Values.serviceMonitor.namespace | default .Release.Namespace }} 39 | roleRef: 40 | apiGroup: rbac.authorization.k8s.io 41 | kind: Role 42 | name: {{ include "capsule-proxy.fullname" . }}-metrics-role 43 | subjects: 44 | - kind: ServiceAccount 45 | name: {{ .Values.serviceMonitor.serviceAccount.name | default (include "capsule-proxy.serviceAccountName" $) }} 46 | namespace: {{ .Values.serviceMonitor.serviceAccount.namespace | default .Release.Namespace }} 47 | {{- end }} 48 | {{- end }} 49 | -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/metrics-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "capsule-proxy.fullname" . }}-metrics-service 5 | labels: 6 | {{- include "capsule-proxy.labels" . | nindent 4 }} 7 | {{- with .Values.customAnnotations }} 8 | annotations: 9 | {{- toYaml . | nindent 4 }} 10 | {{- end }} 11 | spec: 12 | ports: 13 | - port: 8080 14 | name: metrics 15 | protocol: TCP 16 | targetPort: 8080 17 | selector: 18 | {{- include "capsule-proxy.selectorLabels" . | nindent 4 }} 19 | sessionAffinity: None 20 | type: ClusterIP 21 | -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | {{- if $.Values.rbac.enabled }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: {{ include "capsule-proxy.fullname" . }} 6 | labels: 7 | {{- include "capsule-proxy.labels" . | nindent 4 }} 8 | {{- if .Values.rbac.labels }} 9 | {{- toYaml .Values.rbac.labels | nindent 4 }} 10 | {{- end }} 11 | {{- with .Values.rbac.annotations }} 12 | annotations: 13 | {{- toYaml . | nindent 4 }} 14 | {{- end }} 15 | subjects: 16 | - kind: ServiceAccount 17 | name: {{ include "capsule-proxy.serviceAccountName" . }} 18 | namespace: {{ .Release.Namespace }} 19 | roleRef: 20 | kind: ClusterRole 21 | name: {{ $.Values.rbac.clusterRole }} 22 | apiGroup: rbac.authorization.k8s.io 23 | {{- end }} 24 | -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "capsule-proxy.fullname" . }} 5 | labels: 6 | {{- include "capsule-proxy.labels" . | nindent 4 }} 7 | {{- if .Values.service.labels }} 8 | {{ toYaml .Values.service.labels | indent 4 }} 9 | {{- end }} 10 | {{- with .Values.service.annotations }} 11 | annotations: 12 | {{ toYaml . | indent 4 }} 13 | {{- end }} 14 | spec: 15 | {{- if (or (eq .Values.service.type "ClusterIP") (empty .Values.service.type)) }} 16 | type: ClusterIP 17 | {{- if .Values.service.clusterIP }} 18 | clusterIP: {{ .Values.service.clusterIP }} 19 | {{end}} 20 | {{- else if eq .Values.service.type "LoadBalancer" }} 21 | type: {{ .Values.service.type }} 22 | {{- if .Values.service.loadBalancerIP }} 23 | loadBalancerIP: {{ .Values.service.loadBalancerIP }} 24 | {{- end }} 25 | {{- if .Values.service.loadBalancerSourceRanges }} 26 | loadBalancerSourceRanges: 27 | {{ toYaml .Values.service.loadBalancerSourceRanges | indent 4 }} 28 | {{- end -}} 29 | {{- else }} 30 | type: {{ .Values.service.type }} 31 | {{- end }} 32 | ports: 33 | - name: {{ .Values.service.portName }} 34 | port: {{ .Values.service.port }} 35 | protocol: TCP 36 | targetPort: {{ .Values.options.listeningPort }} 37 | {{ if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.nodePort))) }} 38 | nodePort: {{.Values.service.nodePort}} 39 | {{ end }} 40 | selector: 41 | {{- include "capsule-proxy.selectorLabels" . | nindent 4 }} 42 | -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "capsule-proxy.serviceAccountName" . }} 6 | labels: 7 | {{- include "capsule-proxy.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceMonitor.enabled }} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | name: {{ include "capsule-proxy.fullname" . }}-monitor 6 | namespace: {{ .Values.serviceMonitor.namespace | default .Release.Namespace }} 7 | labels: 8 | {{- include "capsule-proxy.labels" . | nindent 4 }} 9 | {{- with .Values.serviceMonitor.labels }} 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- with .Values.serviceMonitor.annotations }} 13 | annotations: 14 | {{- toYaml . | nindent 4 }} 15 | {{- end }} 16 | spec: 17 | endpoints: 18 | {{- with .Values.serviceMonitor.endpoint }} 19 | - interval: {{ .interval }} 20 | port: metrics 21 | path: /metrics 22 | {{- with .scrapeTimeout }} 23 | scrapeTimeout: {{ . }} 24 | {{- end }} 25 | {{- with .metricRelabelings }} 26 | metricRelabelings: {{- toYaml . | nindent 6 }} 27 | {{- end }} 28 | {{- with .relabelings }} 29 | relabelings: {{- toYaml . | nindent 6 }} 30 | {{- end }} 31 | {{- end }} 32 | jobLabel: app.kubernetes.io/name 33 | {{- with .Values.serviceMonitor.targetLabels }} 34 | targetLabels: {{- toYaml . | nindent 4 }} 35 | {{- end }} 36 | selector: 37 | matchLabels: 38 | {{- if .Values.serviceMonitor.matchLabels }} 39 | {{- toYaml .Values.serviceMonitor.matchLabels | nindent 6 }} 40 | {{- else }} 41 | {{- include "capsule-proxy.labels" . | nindent 6 }} 42 | {{- end }} 43 | namespaceSelector: 44 | matchNames: 45 | - {{ .Release.Namespace }} 46 | {{- end }} 47 | -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/webhooks/certificate.yaml: -------------------------------------------------------------------------------- 1 | {{- if $.Values.webhooks.enabled }} 2 | --- 3 | apiVersion: cert-manager.io/v1 4 | kind: Issuer 5 | metadata: 6 | name: {{ include "capsule-proxy.fullname" . }}-webhook-issuer 7 | spec: 8 | selfSigned: {} 9 | --- 10 | apiVersion: cert-manager.io/v1 11 | kind: Certificate 12 | metadata: 13 | name: {{ include "capsule-proxy.fullname" . }}-webhook-ca 14 | spec: 15 | isCA: true 16 | commonName: {{ include "capsule-proxy.fullname" . }}-webhook-ca 17 | secretName: {{ include "capsule-proxy.fullname" . }}-webhook-ca 18 | privateKey: 19 | algorithm: ECDSA 20 | size: 256 21 | issuerRef: 22 | name: {{ include "capsule-proxy.fullname" . }}-webhook-issuer 23 | kind: Issuer 24 | group: cert-manager.io 25 | --- 26 | apiVersion: cert-manager.io/v1 27 | kind: Issuer 28 | metadata: 29 | name: {{ include "capsule-proxy.fullname" . }}-webhook 30 | spec: 31 | ca: 32 | secretName: {{ include "capsule-proxy.fullname" . }}-webhook-ca 33 | --- 34 | apiVersion: cert-manager.io/v1 35 | kind: Certificate 36 | metadata: 37 | name: {{ include "capsule-proxy.fullname" . }}-webhook-cert 38 | spec: 39 | {{- with .Values.webhooks.certificate.fields }} 40 | {{ toYaml . | nindent 2 }} 41 | {{- end }} 42 | dnsNames: 43 | {{- range $dns := .Values.webhooks.certificate.dnsNames }} 44 | - {{ $dns | quote }} 45 | {{- end }} 46 | - {{ include "capsule-proxy.fullname" . }}-webhook-service 47 | - {{ include "capsule-proxy.fullname" . }}-webhook-service.{{ .Release.Namespace }}.svc 48 | {{- with .Values.webhooks.certificate.ipAddresses }} 49 | ipAddresses: 50 | {{- range $ip := . }} 51 | - {{ $ip }} 52 | {{- end }} 53 | {{- end }} 54 | {{- with .Values.webhooks.certificate.uris }} 55 | uris: 56 | {{- range $uri := . }} 57 | - {{ $uri }} 58 | {{- end }} 59 | {{- end }} 60 | issuerRef: 61 | kind: "Issuer" 62 | name: {{ include "capsule-proxy.fullname" . }}-webhook 63 | secretName: {{ include "capsule-proxy.fullname" . }}-webhook-cert 64 | subject: 65 | organizations: 66 | - projectcapsule.dev 67 | {{- end }} -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/webhooks/mutating.yaml: -------------------------------------------------------------------------------- 1 | {{- if $.Values.webhooks.enabled }} 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: MutatingWebhookConfiguration 4 | metadata: 5 | name: {{ include "capsule-proxy.fullname" . }}-webhook 6 | labels: 7 | {{- include "capsule-proxy.labels" . | nindent 4 }} 8 | annotations: 9 | cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "capsule-proxy.fullname" . }}-webhook-cert 10 | webhooks: 11 | {{- with .Values.webhooks.watchdog }} 12 | {{- if .enabled }} 13 | - admissionReviewVersions: 14 | - v1 15 | clientConfig: 16 | {{- include "capsule-proxy.webhooks.service" (dict "path" "/mutate/watchdog" "ctx" $) | nindent 4 }} 17 | failurePolicy: {{ .failurePolicy }} 18 | name: watchdog.proxy.projectcapsule.dev 19 | {{- with .rules }} 20 | rules: 21 | {{- toYaml .| nindent 4}} 22 | {{- end }} 23 | {{- with .namespaceSelector }} 24 | namespaceSelector: 25 | {{- toYaml .| nindent 4}} 26 | {{- end }} 27 | sideEffects: None 28 | timeoutSeconds: {{ $.Values.webhooks.mutatingWebhooksTimeoutSeconds }} 29 | {{- end }} 30 | {{- end }} 31 | {{- end }} 32 | -------------------------------------------------------------------------------- /charts/capsule-proxy/templates/webhooks/service.yaml: -------------------------------------------------------------------------------- 1 | {{- if $.Values.webhooks.enabled }} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ include "capsule-proxy.fullname" . }}-webhook-service 6 | labels: 7 | {{- include "capsule-proxy.labels" . | nindent 4 }} 8 | spec: 9 | ports: 10 | - port: 443 11 | name: https 12 | protocol: TCP 13 | targetPort: {{ .Values.options.webhookPort }} 14 | selector: 15 | {{- include "capsule-proxy.selectorLabels" . | nindent 4 }} 16 | sessionAffinity: None 17 | type: ClusterIP 18 | {{- end }} -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | const Configuration = { 2 | extends: ['@commitlint/config-conventional'], 3 | plugins: ['commitlint-plugin-function-rules'], 4 | rules: { 5 | 'type-enum': [2, 'always', ['chore', 'ci', 'docs', 'feat', 'test', 'fix', 'sec']], 6 | 'body-max-line-length': [1, 'always', 500], 7 | }, 8 | /* 9 | * Whether commitlint uses the default ignore rules, see the description above. 10 | */ 11 | defaultIgnores: true, 12 | /* 13 | * Custom URL to show upon failure 14 | */ 15 | helpUrl: 16 | 'https://github.com/projectcapsule/capsule-proxy/blob/main/CONTRIBUTING.md#commits', 17 | }; 18 | 19 | module.exports = Configuration; -------------------------------------------------------------------------------- /e2e-legacy/curl-http-tests/00_root.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_TEST_DIRNAME/../libs/poll.bash" 4 | load "$BATS_TEST_DIRNAME/../libs/serviceaccount_utils.bash" 5 | 6 | setup() { 7 | create_serviceaccount sa default 8 | token=$(KUBECONFIG=${HACK_DIR}/sa.kubeconfig kubectl config view -o json --raw -o jsonpath='{.users[?(@.name == "sa")].user.token}') 9 | endpoint=http://127.0.0.1:9001 10 | } 11 | 12 | @test "Checking api-resources" { 13 | run sh -c "curl -s -H \"Authorization: Bearer $token\" $endpoint/apis" 14 | [ $status -eq 0 ] 15 | } 16 | 17 | @test "Checking version" { 18 | run sh -c "curl -s -H \"Authorization: Bearer $token\" $endpoint/version" 19 | [ $status -eq 0 ] 20 | } -------------------------------------------------------------------------------- /e2e-legacy/curl-http-tests/namespaces/get.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_TEST_DIRNAME/../../libs/namespaces_utils.bash" 4 | load "$BATS_TEST_DIRNAME/../../libs/poll.bash" 5 | load "$BATS_TEST_DIRNAME/../../libs/tenants_utils.bash" 6 | load "$BATS_TEST_DIRNAME/../../libs/serviceaccount_utils.bash" 7 | load "$BATS_TEST_DIRNAME/../../libs/rolebinding_utils.bash" 8 | 9 | setup() { 10 | create_tenant oil alice User 11 | create_namespace alice oil-dev 12 | create_namespace alice oil-staging 13 | create_namespace alice oil-production 14 | kubectl patch tenants.capsule.clastix.io oil --type=json -p '[{"op": "add", "path": "/spec/owners/1", "value": {"kind": "ServiceAccount", "name": "system:serviceaccount:default:sa"}}]' 15 | create_serviceaccount sa default 16 | token=$(KUBECONFIG=${HACK_DIR}/sa.kubeconfig kubectl config view -o json --raw -o jsonpath='{.users[?(@.name == "sa")].user.token}') 17 | endpoint=http://127.0.0.1:9001 18 | } 19 | 20 | teardown() { 21 | delete_tenant oil 22 | } 23 | 24 | @test "List allowed namespace" { 25 | poll_until_equals "SA" "oil-dev" "curl -s -H \"Authorization: Bearer $token\" $endpoint/api/v1/namespaces/oil-dev | jq -r '.metadata.name'" 3 5 26 | poll_until_equals "SA" "oil-production" "curl -s -H \"Authorization: Bearer $token\" $endpoint/api/v1/namespaces/oil-production | jq -r '.metadata.name'" 3 5 27 | poll_until_equals "SA" "oil-staging" "curl -s -H \"Authorization: Bearer $token\" $endpoint/api/v1/namespaces/oil-staging | jq -r '.metadata.name'" 3 5 28 | } 29 | 30 | @test "List non-allowed namespaces" { 31 | poll_until_equals "SA" "Forbidden" "curl -s -H \"Authorization: Bearer $token\" $endpoint/api/v1/namespaces/kube-system | jq -r '.reason'" 3 5 32 | } -------------------------------------------------------------------------------- /e2e-legacy/curl-http-tests/namespaces/list.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_TEST_DIRNAME/../../libs/namespaces_utils.bash" 4 | load "$BATS_TEST_DIRNAME/../../libs/poll.bash" 5 | load "$BATS_TEST_DIRNAME/../../libs/tenants_utils.bash" 6 | load "$BATS_TEST_DIRNAME/../../libs/serviceaccount_utils.bash" 7 | load "$BATS_TEST_DIRNAME/../../libs/rolebinding_utils.bash" 8 | 9 | setup() { 10 | create_tenant oil alice User 11 | create_namespace alice oil-dev 12 | create_namespace alice oil-staging 13 | create_namespace alice oil-production 14 | kubectl patch tenants.capsule.clastix.io oil --type=json -p '[{"op": "add", "path": "/spec/owners/1", "value": {"kind": "ServiceAccount", "name": "system:serviceaccount:default:sa"}}]' 15 | create_serviceaccount sa default 16 | token=$(KUBECONFIG=${HACK_DIR}/sa.kubeconfig kubectl config view -o json --raw -o jsonpath='{.users[?(@.name == "sa")].user.token}') 17 | endpoint=http://127.0.0.1:9001 18 | } 19 | 20 | teardown() { 21 | delete_tenant oil 22 | } 23 | 24 | @test "List allowed namespaces" { 25 | local sa="oil-dev 26 | oil-production 27 | oil-staging" 28 | poll_until_equals "SA" "$sa" "curl -s -H "Authorization: Bearer $token" $endpoint/api/v1/namespaces | jq -r '.items[].metadata.name'" 3 5 29 | } 30 | -------------------------------------------------------------------------------- /e2e-legacy/curl-https-tests/00_root.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_TEST_DIRNAME/../libs/poll.bash" 4 | load "$BATS_TEST_DIRNAME/../libs/serviceaccount_utils.bash" 5 | 6 | setup() { 7 | create_serviceaccount sa default 8 | token=$(KUBECONFIG=${HACK_DIR}/sa.kubeconfig kubectl config view -o json --raw -o jsonpath='{.users[?(@.name == "sa")].user.token}') 9 | endpoint=$(KUBECONFIG=${HACK_DIR}/sa.kubeconfig kubectl config view -o json --raw -o jsonpath='{.clusters[?(@.name == "kind-capsule")].cluster.server}') 10 | } 11 | @test "Checking api-resources" { 12 | run sh -c "curl -s -H \"Authorization: Bearer $token\" $endpoint/apis" 13 | [ $status -eq 0 ] 14 | } 15 | 16 | @test "Checking version" { 17 | run sh -c "curl -s -H \"Authorization: Bearer $token\" $endpoint/version" 18 | [ $status -eq 0 ] 19 | } -------------------------------------------------------------------------------- /e2e-legacy/curl-https-tests/namespaces/get.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_TEST_DIRNAME/../../libs/namespaces_utils.bash" 4 | load "$BATS_TEST_DIRNAME/../../libs/poll.bash" 5 | load "$BATS_TEST_DIRNAME/../../libs/tenants_utils.bash" 6 | load "$BATS_TEST_DIRNAME/../../libs/serviceaccount_utils.bash" 7 | load "$BATS_TEST_DIRNAME/../../libs/rolebinding_utils.bash" 8 | 9 | setup() { 10 | create_tenant oil alice User 11 | create_namespace alice oil-dev 12 | create_namespace alice oil-staging 13 | create_namespace alice oil-production 14 | kubectl patch tenants.capsule.clastix.io oil --type=json -p '[{"op": "add", "path": "/spec/owners/1", "value": {"kind": "ServiceAccount", "name": "system:serviceaccount:default:sa"}}]' 15 | create_serviceaccount sa default 16 | token=$(KUBECONFIG=${HACK_DIR}/sa.kubeconfig kubectl config view -o json --raw -o jsonpath='{.users[?(@.name == "sa")].user.token}') 17 | endpoint=$(KUBECONFIG=${HACK_DIR}/sa.kubeconfig kubectl config view -o json --raw -o jsonpath='{.clusters[?(@.name == "kind-capsule")].cluster.server}') 18 | } 19 | 20 | teardown() { 21 | delete_tenant oil 22 | } 23 | 24 | @test "List allowed namespace" { 25 | poll_until_equals "SA" "oil-dev" "curl -s -H \"Authorization: Bearer $token\" $endpoint/api/v1/namespaces/oil-dev | jq -r '.metadata.name'" 3 5 26 | poll_until_equals "SA" "oil-production" "curl -s -H \"Authorization: Bearer $token\" $endpoint/api/v1/namespaces/oil-production | jq -r '.metadata.name'" 3 5 27 | poll_until_equals "SA" "oil-staging" "curl -s -H \"Authorization: Bearer $token\" $endpoint/api/v1/namespaces/oil-staging | jq -r '.metadata.name'" 3 5 28 | } 29 | 30 | @test "List non-allowed namespaces" { 31 | poll_until_equals "SA" "Forbidden" "curl -s -H \"Authorization: Bearer $token\" $endpoint/api/v1/namespaces/kube-system | jq -r '.reason'" 3 5 32 | } -------------------------------------------------------------------------------- /e2e-legacy/curl-https-tests/namespaces/list.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_TEST_DIRNAME/../../libs/namespaces_utils.bash" 4 | load "$BATS_TEST_DIRNAME/../../libs/poll.bash" 5 | load "$BATS_TEST_DIRNAME/../../libs/tenants_utils.bash" 6 | load "$BATS_TEST_DIRNAME/../../libs/serviceaccount_utils.bash" 7 | load "$BATS_TEST_DIRNAME/../../libs/rolebinding_utils.bash" 8 | 9 | setup() { 10 | create_tenant oil alice User 11 | create_namespace alice oil-dev 12 | create_namespace alice oil-staging 13 | create_namespace alice oil-production 14 | kubectl patch tenants.capsule.clastix.io oil --type=json -p '[{"op": "add", "path": "/spec/owners/1", "value": {"kind": "ServiceAccount", "name": "system:serviceaccount:default:sa"}}]' 15 | create_serviceaccount sa default 16 | token=$(KUBECONFIG=${HACK_DIR}/sa.kubeconfig kubectl config view -o json --raw -o jsonpath='{.users[?(@.name == "sa")].user.token}') 17 | endpoint=$(KUBECONFIG=${HACK_DIR}/sa.kubeconfig kubectl config view -o json --raw -o jsonpath='{.clusters[?(@.name == "kind-capsule")].cluster.server}') 18 | } 19 | 20 | teardown() { 21 | delete_tenant oil 22 | } 23 | 24 | @test "List allowed namespaces" { 25 | local sa="oil-dev 26 | oil-production 27 | oil-staging" 28 | poll_until_equals "SA" "$sa" "curl -s -H "Authorization: Bearer $token" $endpoint/api/v1/namespaces | jq -r '.items[].metadata.name'" 3 5 29 | } 30 | -------------------------------------------------------------------------------- /e2e-legacy/kind.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kind.x-k8s.io/v1alpha4 2 | networking: 3 | apiServerAddress: "127.0.0.1" 4 | apiServerPort: 6443 5 | kind: Cluster 6 | nodes: 7 | - role: control-plane 8 | - role: worker 9 | extraPortMappings: 10 | - hostPort: 9001 11 | containerPort: 9001 12 | - role: worker 13 | extraPortMappings: 14 | - hostPort: 9002 15 | containerPort: 9002 16 | -------------------------------------------------------------------------------- /e2e-legacy/kubectl-http-tests/00_root.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_TEST_DIRNAME/../libs/poll.bash" 4 | 5 | @test "Checking kubectl api-resources" { 6 | run sh -c "KUBECONFIG=${HACK_DIR}/alice.kubeconfig kubectl api-resources" 7 | [ $status -ne 0 ] 8 | } 9 | 10 | @test "Checking kubectl version" { 11 | run sh -c "KUBECONFIG=${HACK_DIR}/alice.kubeconfig kubectl version" 12 | [ $status -ne 0 ] 13 | } 14 | -------------------------------------------------------------------------------- /e2e-legacy/kubectl-http-tests/namespaces/list.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_TEST_DIRNAME/../../libs/poll.bash" 4 | 5 | @test "List namespaces" { 6 | namespaces=$(kubectl get ns -o name) 7 | poll_until_different "User" "$namespaces" "kubectl --kubeconfig=${HACK_DIR}/alice.kubeconfig get namespaces --output=name" 3 5 8 | } 9 | -------------------------------------------------------------------------------- /e2e-legacy/kubectl-https-tests/00_root.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_TEST_DIRNAME/../libs/poll.bash" 4 | 5 | @test "Checking kubectl api-resources" { 6 | run sh -c "KUBECONFIG=${HACK_DIR}/alice.kubeconfig kubectl api-resources" 7 | [ $status -eq 0 ] 8 | } 9 | 10 | @test "Checking kubectl version" { 11 | run sh -c "KUBECONFIG=${HACK_DIR}/alice.kubeconfig kubectl version" 12 | [ $status -eq 0 ] 13 | } 14 | -------------------------------------------------------------------------------- /e2e-legacy/libs/ingressclass_utils.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function create_ingressclass() { 4 | local version name 5 | version=${1} 6 | name=${2} 7 | cat <>> Waiting for $what " >&3 31 | local count=0 32 | until eval "$check_cmd"; do 33 | echo -n '.' >&3 34 | sleep "$wait_period" 3>&- 35 | count=$((count + 1)) 36 | if [[ ${count} -eq ${retries} ]]; then 37 | echo ': no more retries left!' >&3 38 | return 1 # fail 39 | fi 40 | done 41 | echo ': done' >&3 42 | } 43 | -------------------------------------------------------------------------------- /e2e-legacy/libs/priorityclass_utils.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function create_priorityclass() { 4 | local name 5 | name=${1} 6 | cat <&- apply -f - 11 | apiVersion: capsule.clastix.io/v1beta1 12 | kind: ProxySetting 13 | metadata: 14 | name: ${name} 15 | namespace: ${namespace} 16 | spec: 17 | subjects: 18 | - name: ${owner} 19 | kind: ${ownerKind} 20 | EOF 21 | 22 | } 23 | 24 | function delete_proxysetting() { 25 | local name namespace 26 | name=${1} 27 | namespace=${2} 28 | 29 | kubectl delete proxysettings.capsule.clastix.io "${name}" -n "${namespace}" 30 | } 31 | -------------------------------------------------------------------------------- /e2e-legacy/libs/rolebinding_utils.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function create_rolebinding() { 4 | local username namespace 5 | name=${1} 6 | kind=${2} 7 | namespace=${3} 8 | 9 | cat < hack/sa.kubeconfig <&- apply -f - 10 | apiVersion: capsule.clastix.io/v1beta1 11 | kind: Tenant 12 | metadata: 13 | name: ${name} 14 | spec: 15 | owners: 16 | - kind: ${ownerKind} 17 | name: ${owner} 18 | EOF 19 | 20 | } 21 | 22 | function delete_tenant() { 23 | local name 24 | name=${1} 25 | 26 | kubectl get tenants.capsule.clastix.io "${name}" -o=jsonpath='{.status.namespaces}' | jq ".[]" | xargs -L1 -I'{}' kubectl delete namespace {} 27 | kubectl delete tenants.capsule.clastix.io "${name}" 28 | } 29 | -------------------------------------------------------------------------------- /e2e-legacy/run.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | TESTS=$1 5 | 6 | HACK_DIR="$(git rev-parse --show-toplevel)/hack" 7 | export HACK_DIR 8 | 9 | echo ">>> Waiting for capsule-proxy pod to be ready for accepting requests" 10 | kubectl --namespace capsule-system wait --for=condition=ready --timeout=320s pod -l app.kubernetes.io/name=capsule-proxy 11 | 12 | echo ">>> Waiting for capsule pod to be ready for accepting requests" 13 | kubectl --namespace capsule-system wait --for=condition=ready --timeout=320s pod -l app.kubernetes.io/instance=capsule 14 | 15 | echo ">>> Waiting for metrics-server pod to be ready for listing metrics" 16 | kubectl --namespace metrics-system wait --for=condition=ready --timeout=320s pod -l app.kubernetes.io/instance=metrics-server 17 | until [[ $(kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes" 2>/dev/null | jq type 2>/dev/null) == "\"object\"" ]] 18 | do 19 | printf "." 20 | sleep 5 21 | done 22 | until [[ $(kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes" 2>/dev/null | jq '.items' 2>/dev/null | jq length 2>/dev/null) == $(kubectl get nodes -o=name | wc -l) ]] 23 | do 24 | printf "." 25 | sleep 5 26 | done 27 | 28 | echo -e "\n>>> Starting test suite ${TESTS}" 29 | bats -t "$(git rev-parse --show-toplevel)"/e2e/${TESTS}-tests/* 30 | -------------------------------------------------------------------------------- /e2e/distro/flux/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - https://github.com/fluxcd/flux2/releases/download/v2.4.0/install.yaml 5 | patches: 6 | - patch: | 7 | - op: add 8 | path: /spec/template/spec/containers/0/args/- 9 | value: --no-cross-namespace-refs=true 10 | target: 11 | kind: Deployment 12 | name: "(kustomize-controller|helm-controller|notification-controller|image-reflector-controller|image-automation-controller)" 13 | - patch: | 14 | - op: add 15 | path: /spec/template/spec/containers/0/args/- 16 | value: --no-remote-bases=true 17 | target: 18 | kind: Deployment 19 | name: "kustomize-controller" 20 | - patch: | 21 | - op: add 22 | path: /spec/template/spec/containers/0/args/- 23 | value: --default-service-account=default 24 | target: 25 | kind: Deployment 26 | name: "(kustomize-controller|helm-controller)" 27 | - patch: | 28 | - op: replace 29 | path: /spec/replicas 30 | value: 0 31 | target: 32 | kind: Deployment 33 | name: "(notification-controller|image-reflector-controller|image-automation-controller)" 34 | -------------------------------------------------------------------------------- /e2e/distro/objects/capsule.flux.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: source.toolkit.fluxcd.io/v1 3 | kind: HelmRepository 4 | metadata: 5 | name: projectcapsule 6 | spec: 7 | interval: 30s 8 | url: https://projectcapsule.github.io/charts 9 | --- 10 | apiVersion: helm.toolkit.fluxcd.io/v2 11 | kind: HelmRelease 12 | metadata: 13 | name: capsule 14 | spec: 15 | serviceAccountName: kustomize-controller 16 | interval: 30s 17 | targetNamespace: capsule-system 18 | releaseName: "capsule" 19 | chart: 20 | spec: 21 | chart: capsule 22 | version: "0.10.0" 23 | sourceRef: 24 | kind: HelmRepository 25 | name: projectcapsule 26 | interval: 24h 27 | install: 28 | createNamespace: true 29 | remediation: 30 | retries: -1 31 | upgrade: 32 | remediation: 33 | remediateLastFailure: true 34 | driftDetection: 35 | mode: enabled 36 | values: 37 | crds: 38 | install: true 39 | manager: 40 | resources: null 41 | options: 42 | forceTenantPrefix: false 43 | options: 44 | logLevel: 8 45 | -------------------------------------------------------------------------------- /e2e/distro/objects/cert-manager.flux.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: source.toolkit.fluxcd.io/v1 3 | kind: HelmRepository 4 | metadata: 5 | name: cert-manager 6 | namespace: flux-system 7 | spec: 8 | interval: 30s 9 | url: https://charts.jetstack.io 10 | --- 11 | apiVersion: helm.toolkit.fluxcd.io/v2 12 | kind: HelmRelease 13 | metadata: 14 | name: cert-manager 15 | spec: 16 | serviceAccountName: kustomize-controller 17 | interval: 30s 18 | releaseName: "cert-manager" 19 | targetNamespace: "cert-manager" 20 | chart: 21 | spec: 22 | chart: cert-manager 23 | version: "1.15.3" 24 | sourceRef: 25 | kind: HelmRepository 26 | name: cert-manager 27 | interval: 24h 28 | install: 29 | createNamespace: true 30 | upgrade: 31 | remediation: 32 | remediateLastFailure: true 33 | retries: -1 34 | driftDetection: 35 | mode: enabled 36 | values: 37 | crds: 38 | enabled: true 39 | -------------------------------------------------------------------------------- /e2e/distro/objects/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: flux-system 4 | resources: 5 | - https://github.com/prometheus-operator/prometheus-operator/releases/download/v0.58.0/bundle.yaml 6 | - cert-manager.flux.yaml 7 | - capsule.flux.yaml 8 | - metrics.flux.yaml 9 | -------------------------------------------------------------------------------- /e2e/distro/objects/metrics.flux.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: source.toolkit.fluxcd.io/v1 3 | kind: HelmRepository 4 | metadata: 5 | name: metrics-server 6 | spec: 7 | interval: 30s 8 | url: https://kubernetes-sigs.github.io/metrics-server/ 9 | --- 10 | apiVersion: helm.toolkit.fluxcd.io/v2 11 | kind: HelmRelease 12 | metadata: 13 | name: metrics-server 14 | spec: 15 | serviceAccountName: kustomize-controller 16 | interval: 1m 17 | releaseName: "metrics-server" 18 | targetNamespace: "kube-system" 19 | chart: 20 | spec: 21 | chart: metrics-server 22 | version: "3.12.2" 23 | sourceRef: 24 | kind: HelmRepository 25 | name: metrics-server 26 | interval: 24h 27 | install: 28 | createNamespace: false 29 | remediation: 30 | retries: -1 31 | upgrade: 32 | remediation: 33 | remediateLastFailure: true 34 | driftDetection: 35 | mode: enabled 36 | values: 37 | args: 38 | - --kubelet-insecure-tls 39 | -------------------------------------------------------------------------------- /e2e/e2e_suite_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestE2e(t *testing.T) { 11 | t.Parallel() 12 | RegisterFailHandler(Fail) 13 | RunSpecs(t, "E2e Suite") 14 | } 15 | -------------------------------------------------------------------------------- /e2e/kind.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kind.x-k8s.io/v1alpha4 2 | kind: Cluster 3 | # networking: 4 | # apiServerAddress: "127.0.0.1" 5 | # apiServerPort: 6443 6 | nodes: 7 | - role: control-plane 8 | - role: worker 9 | extraPortMappings: 10 | - hostPort: 9001 11 | containerPort: 9001 12 | - role: worker 13 | -------------------------------------------------------------------------------- /e2e/suite_client_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | type e2eClient struct { 11 | client.Client 12 | } 13 | 14 | func (e *e2eClient) sleep() { 15 | time.Sleep(250 * time.Millisecond) 16 | } 17 | 18 | func (e *e2eClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { 19 | defer e.sleep() 20 | 21 | return e.Client.Get(ctx, key, obj, opts...) 22 | } 23 | 24 | func (e *e2eClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { 25 | defer e.sleep() 26 | 27 | return e.Client.List(ctx, list, opts...) 28 | } 29 | 30 | func (e *e2eClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { 31 | defer e.sleep() 32 | 33 | return e.Client.Create(ctx, obj, opts...) 34 | } 35 | 36 | func (e *e2eClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { 37 | defer e.sleep() 38 | 39 | return e.Client.Delete(ctx, obj, opts...) 40 | } 41 | 42 | func (e *e2eClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 43 | defer e.sleep() 44 | 45 | return e.Client.Update(ctx, obj, opts...) 46 | } 47 | 48 | func (e *e2eClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { 49 | defer e.sleep() 50 | 51 | return e.Client.Patch(ctx, obj, patch, opts...) 52 | } 53 | 54 | func (e *e2eClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { 55 | defer e.sleep() 56 | 57 | return e.Client.DeleteAllOf(ctx, obj, opts...) 58 | } 59 | -------------------------------------------------------------------------------- /e2e/suite_test.go: -------------------------------------------------------------------------------- 1 | //nolint:all 2 | package e2e_test 3 | 4 | import ( 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" 8 | "k8s.io/client-go/rest" 9 | "k8s.io/kubectl/pkg/scheme" 10 | "k8s.io/utils/ptr" 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | "sigs.k8s.io/controller-runtime/pkg/envtest" 13 | logf "sigs.k8s.io/controller-runtime/pkg/log" 14 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 15 | 16 | v1beta1 "github.com/projectcapsule/capsule-proxy/api/v1beta1" 17 | ) 18 | 19 | //nolint:gochecknoglobals 20 | var ( 21 | cfg *rest.Config 22 | k8sClient client.Client 23 | testEnv *envtest.Environment 24 | ) 25 | 26 | var _ = BeforeSuite(func() { 27 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter))) 28 | 29 | By("bootstrapping test environment") 30 | testEnv = &envtest.Environment{ 31 | UseExistingCluster: ptr.To(true), 32 | } 33 | 34 | var err error 35 | cfg, err = testEnv.Start() 36 | Expect(err).ToNot(HaveOccurred()) 37 | Expect(cfg).ToNot(BeNil()) 38 | 39 | Expect(v1beta1.AddToScheme(scheme.Scheme)).NotTo(HaveOccurred()) 40 | Expect(capsulev1beta2.AddToScheme(scheme.Scheme)).NotTo(HaveOccurred()) 41 | 42 | ctrlClient, err := client.New(cfg, client.Options{Scheme: scheme.Scheme}) 43 | Expect(err).ToNot(HaveOccurred()) 44 | Expect(ctrlClient).ToNot(BeNil()) 45 | 46 | k8sClient = &e2eClient{Client: ctrlClient} 47 | }) 48 | 49 | var _ = AfterSuite(func() { 50 | By("tearing down the test environment") 51 | Expect(testEnv.Stop()).ToNot(HaveOccurred()) 52 | }) 53 | -------------------------------------------------------------------------------- /hack/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !boilerplate.go.txt 3 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | -------------------------------------------------------------------------------- /internal/controllers/capsule_configuration.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package controllers 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | "github.com/pkg/errors" 11 | capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" 12 | k8serrors "k8s.io/apimachinery/pkg/api/errors" 13 | "k8s.io/apimachinery/pkg/types" 14 | "k8s.io/apimachinery/pkg/util/sets" 15 | ctrl "sigs.k8s.io/controller-runtime" 16 | "sigs.k8s.io/controller-runtime/pkg/builder" 17 | "sigs.k8s.io/controller-runtime/pkg/client" 18 | "sigs.k8s.io/controller-runtime/pkg/predicate" 19 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 20 | ) 21 | 22 | type CapsuleConfiguration struct { 23 | Client client.Client 24 | CapsuleConfigurationName string 25 | DeprecatedCapsuleUserGroups []string 26 | } 27 | 28 | //nolint:gochecknoglobals 29 | var CapsuleUserGroups sets.Set[string] 30 | 31 | func (c *CapsuleConfiguration) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error { 32 | if len(c.DeprecatedCapsuleUserGroups) > 0 { 33 | CapsuleUserGroups = sets.New[string](c.DeprecatedCapsuleUserGroups...) 34 | 35 | return nil 36 | } 37 | 38 | if err := mgr.GetAPIReader().Get(ctx, types.NamespacedName{Name: c.CapsuleConfigurationName}, &capsulev1beta2.CapsuleConfiguration{}); err != nil { 39 | if k8serrors.IsNotFound(err) { 40 | return fmt.Errorf("CapsuleConfiguration %s does not exist", c.CapsuleConfigurationName) 41 | } 42 | 43 | return errors.Wrap(err, "unable to retrieve CapsuleConfiguration") 44 | } 45 | 46 | return ctrl.NewControllerManagedBy(mgr). 47 | For(&capsulev1beta2.CapsuleConfiguration{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool { 48 | return object.GetName() == c.CapsuleConfigurationName 49 | }))). 50 | Complete(c) 51 | } 52 | 53 | func (c *CapsuleConfiguration) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { 54 | capsuleConfig := &capsulev1beta2.CapsuleConfiguration{} 55 | 56 | if err := c.Client.Get(ctx, types.NamespacedName{Name: request.Name}, capsuleConfig); err != nil { 57 | panic(err) 58 | } 59 | 60 | CapsuleUserGroups = sets.New(capsuleConfig.Spec.UserGroups...) 61 | 62 | return reconcile.Result{}, nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/controllers/watchdog/utils.go: -------------------------------------------------------------------------------- 1 | package watchdog 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/pkg/errors" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "k8s.io/apimachinery/pkg/util/sets" 9 | "k8s.io/client-go/discovery" 10 | "k8s.io/client-go/rest" 11 | 12 | "github.com/projectcapsule/capsule-proxy/internal/utils" 13 | ) 14 | 15 | func API(config *rest.Config) ([]utils.ProxyGroupVersionKind, error) { 16 | discoveryClient := discovery.NewDiscoveryClientForConfigOrDie(config) 17 | 18 | apiResourceLists, err := discoveryClient.ServerPreferredNamespacedResources() 19 | if err != nil { 20 | return nil, errors.Wrap(err, "cannot retrieve server's preferred namespaced resources") 21 | } 22 | 23 | var out []utils.ProxyGroupVersionKind 24 | 25 | for _, ar := range apiResourceLists { 26 | parts := strings.Split(ar.GroupVersion, "/") 27 | 28 | var group, version string 29 | 30 | if len(parts) == 1 { 31 | group = "" 32 | version = ar.GroupVersion 33 | } else { 34 | group = parts[0] 35 | version = parts[1] 36 | } 37 | 38 | for _, i := range ar.APIResources { 39 | if !sets.New[string]([]string(i.Verbs)...).HasAll("get", "list", "watch") { 40 | continue 41 | } 42 | 43 | out = append(out, utils.ProxyGroupVersionKind{ 44 | GroupVersionKind: schema.GroupVersionKind{ 45 | Group: group, 46 | Version: version, 47 | Kind: i.Kind, 48 | }, 49 | URLName: i.Name, 50 | }) 51 | } 52 | } 53 | 54 | return out, nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/features/features.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2021 Clastix Labs 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package features 5 | 6 | const ( 7 | // ProxyAllNamespaced allows to proxy all the Namespaced objects 8 | // for all tenant users 9 | // 10 | // When enabled, it will discover apis and ensure labels are set 11 | // for resources in all tenant namespaces resulting in increased memory 12 | // usage and cluster-wide RBAC permissions (list and watch). 13 | ProxyAllNamespaced = "ProxyAllNamespaced" 14 | 15 | // SkipImpersonationReview allows to skip the impersonation review 16 | // for all requests containing impersonation headers (user and groups) 17 | // 18 | // DANGER: Enabling this flag allows any user to impersonate as any user or group 19 | // essentially bypassing any authorization. Only use this option in trusted environments 20 | // where authorization/authentication is offloaded to external systems. 21 | SkipImpersonationReview = "SkipImpersonationReview" 22 | 23 | // ProxyClusterScoped allows to proxy all clusterScoped objects 24 | // for all tenant users. 25 | ProxyClusterScoped = "ProxyClusterScoped" 26 | ) 27 | -------------------------------------------------------------------------------- /internal/indexer/global_proxy_setting.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package indexer 5 | 6 | import ( 7 | "fmt" 8 | 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | 11 | "github.com/projectcapsule/capsule-proxy/api/v1beta1" 12 | ) 13 | 14 | const ( 15 | GlobalKindField = "spec.subjects.ownerkind" 16 | ) 17 | 18 | // ProxySetting is the indexer that allows retrieving the Capsule Proxy Settings 19 | // for a specific actor according to its kind. 20 | type GlobalProxySetting struct{} 21 | 22 | func (p GlobalProxySetting) Object() client.Object { 23 | return &v1beta1.GlobalProxySettings{} 24 | } 25 | 26 | func (p GlobalProxySetting) Field() string { 27 | return GlobalKindField 28 | } 29 | 30 | func (p GlobalProxySetting) Func() client.IndexerFunc { 31 | return func(object client.Object) (owners []string) { 32 | //nolint:forcetypeassert 33 | proxySetting := object.(*v1beta1.GlobalProxySettings) 34 | 35 | for _, owner := range proxySetting.Spec.Rules { 36 | for _, subject := range owner.Subjects { 37 | if subject.Kind == "" || subject.Name == "" { 38 | continue 39 | } 40 | 41 | owners = append(owners, fmt.Sprintf("%s:%s", subject.Kind.String(), subject.Name)) 42 | } 43 | } 44 | 45 | return 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/indexer/proxy_setting.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package indexer 5 | 6 | import ( 7 | "fmt" 8 | 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | 11 | "github.com/projectcapsule/capsule-proxy/api/v1beta1" 12 | ) 13 | 14 | const ( 15 | SubjectKindField = "spec.subjects.ownerkind" 16 | ) 17 | 18 | // ProxySetting is the indexer that allows retrieving the Capsule Proxy Settings 19 | // for a specific actor according to its kind. 20 | type ProxySetting struct{} 21 | 22 | func (p ProxySetting) Object() client.Object { 23 | return &v1beta1.ProxySetting{} 24 | } 25 | 26 | func (p ProxySetting) Field() string { 27 | return SubjectKindField 28 | } 29 | 30 | func (p ProxySetting) Func() client.IndexerFunc { 31 | return func(object client.Object) (owners []string) { 32 | //nolint:forcetypeassert 33 | proxySetting := object.(*v1beta1.ProxySetting) 34 | 35 | for _, owner := range proxySetting.Spec.Subjects { 36 | owners = append(owners, fmt.Sprintf("%s:%s", owner.Kind.String(), owner.Name)) 37 | } 38 | 39 | return 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/labels/managed.go: -------------------------------------------------------------------------------- 1 | package labels 2 | 3 | const ( 4 | ManagedByCapsuleLabel = "capsule.clastix.io/managed-by" 5 | ) 6 | -------------------------------------------------------------------------------- /internal/modules/clusterscoped/list.go: -------------------------------------------------------------------------------- 1 | package clusterscoped 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-logr/logr" 7 | "k8s.io/apimachinery/pkg/labels" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | "k8s.io/apimachinery/pkg/selection" 10 | ctrl "sigs.k8s.io/controller-runtime" 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | 13 | "github.com/projectcapsule/capsule-proxy/internal/modules" 14 | "github.com/projectcapsule/capsule-proxy/internal/modules/utils" 15 | "github.com/projectcapsule/capsule-proxy/internal/request" 16 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 17 | ) 18 | 19 | type list struct { 20 | path string 21 | log logr.Logger 22 | reader client.Reader 23 | writer client.Writer 24 | } 25 | 26 | func List(client client.Reader, writer client.Writer, path string) modules.Module { 27 | return &list{ 28 | path: path, 29 | log: ctrl.Log.WithName("clusterresource_list"), 30 | reader: client, 31 | writer: writer, 32 | } 33 | } 34 | 35 | func (l list) GroupVersionKind() schema.GroupVersionKind { 36 | return schema.GroupVersionKind{} 37 | } 38 | 39 | func (l list) GroupKind() schema.GroupKind { 40 | return schema.GroupKind{} 41 | } 42 | 43 | func (l list) Path() string { 44 | return l.path 45 | } 46 | 47 | func (l list) Methods() []string { 48 | return []string{} 49 | } 50 | 51 | func (l list) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 52 | httpRequest := proxyRequest.GetHTTPRequest() 53 | 54 | gvk := utils.GetGVKFromURL(proxyRequest.GetHTTPRequest().URL.Path) 55 | 56 | _, requirements := utils.GetClusterScopeRequirements(gvk, proxyTenants) 57 | if len(requirements) > 0 { 58 | switch httpRequest.Method { 59 | case http.MethodGet: 60 | return utils.HandleListSelector(requirements) 61 | default: 62 | return nil, nil 63 | } 64 | } 65 | 66 | r, _ := labels.NewRequirement("dontexistsignoreme", selection.Exists, []string{}) 67 | 68 | return labels.NewSelector().Add(*r), nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/modules/errors/bad_request.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package errors 5 | 6 | import ( 7 | "net/http" 8 | 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | ) 12 | 13 | //nolint:errname 14 | type badRequest struct { 15 | message string 16 | details *metav1.StatusDetails 17 | } 18 | 19 | func NewBadRequest(message error, gk schema.GroupKind) error { 20 | return &badRequest{ 21 | message: message.Error(), 22 | details: &metav1.StatusDetails{ 23 | Group: gk.Group, 24 | Kind: gk.Kind, 25 | }, 26 | } 27 | } 28 | 29 | func (b badRequest) Error() string { 30 | return b.message 31 | } 32 | 33 | func (b badRequest) Status() *metav1.Status { 34 | return &metav1.Status{ 35 | TypeMeta: metav1.TypeMeta{ 36 | Kind: "Status", 37 | APIVersion: "v1", 38 | }, 39 | Reason: metav1.StatusReasonBadRequest, 40 | Message: b.message, 41 | Status: metav1.StatusFailure, 42 | Code: http.StatusBadRequest, 43 | Details: b.details, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/modules/errors/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package errors 5 | 6 | import ( 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | type Error interface { 11 | error 12 | Status() *metav1.Status 13 | } 14 | -------------------------------------------------------------------------------- /internal/modules/errors/not_allowed.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/runtime/schema" 6 | ) 7 | 8 | func NewNotAllowed(gk schema.GroupKind) error { 9 | return &badRequest{ 10 | message: "not allowed", 11 | details: &metav1.StatusDetails{ 12 | Group: gk.Group, 13 | Kind: gk.Kind, 14 | }, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/modules/errors/not_found.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package errors 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | ) 13 | 14 | type notFoundError struct { 15 | message string 16 | details *metav1.StatusDetails 17 | } 18 | 19 | func NewNotFoundError(name string, gk schema.GroupKind) error { 20 | message := fmt.Sprintf("%s.%s %q not found", gk.Kind, gk.Group, name) 21 | 22 | return ¬FoundError{ 23 | message: message, 24 | details: &metav1.StatusDetails{ 25 | Name: name, 26 | Group: gk.Group, 27 | Kind: gk.Kind, 28 | }, 29 | } 30 | } 31 | 32 | func (e notFoundError) Error() string { 33 | return e.message 34 | } 35 | 36 | func (e notFoundError) Status() *metav1.Status { 37 | return &metav1.Status{ 38 | TypeMeta: metav1.TypeMeta{ 39 | Kind: "Status", 40 | APIVersion: "v1", 41 | }, 42 | Reason: metav1.StatusReasonNotFound, 43 | Message: e.message, 44 | Status: metav1.StatusFailure, 45 | Code: http.StatusNotFound, 46 | Details: e.details, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /internal/modules/ingressclass/get.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package ingressclass 5 | 6 | import ( 7 | "net/http" 8 | 9 | "github.com/go-logr/logr" 10 | "github.com/gorilla/mux" 11 | corev1 "k8s.io/api/core/v1" 12 | networkingv1 "k8s.io/api/networking/v1" 13 | "k8s.io/apimachinery/pkg/labels" 14 | "k8s.io/apimachinery/pkg/runtime/schema" 15 | ctrl "sigs.k8s.io/controller-runtime" 16 | "sigs.k8s.io/controller-runtime/pkg/client" 17 | 18 | "github.com/projectcapsule/capsule-proxy/internal/modules" 19 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 20 | "github.com/projectcapsule/capsule-proxy/internal/modules/utils" 21 | "github.com/projectcapsule/capsule-proxy/internal/request" 22 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 23 | ) 24 | 25 | type get struct { 26 | client client.Reader 27 | log logr.Logger 28 | gk schema.GroupVersionKind 29 | } 30 | 31 | func Get(client client.Reader) modules.Module { 32 | return &get{ 33 | client: client, 34 | log: ctrl.Log.WithName("ingressclass_get"), 35 | gk: schema.GroupVersionKind{ 36 | Group: networkingv1.GroupName, 37 | Version: "*", 38 | Kind: "ingressclasses", 39 | }, 40 | } 41 | } 42 | 43 | func (g get) GroupVersionKind() schema.GroupVersionKind { 44 | return g.gk 45 | } 46 | 47 | func (g get) GroupKind() schema.GroupKind { 48 | return g.gk.GroupKind() 49 | } 50 | 51 | func (g get) Path() string { 52 | return "/apis/networking.k8s.io/{version}/{endpoint:ingressclasses}/{name}" 53 | } 54 | 55 | func (g get) Methods() []string { 56 | return []string{} 57 | } 58 | 59 | func (g get) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 60 | httpRequest := proxyRequest.GetHTTPRequest() 61 | 62 | name := mux.Vars(httpRequest)["name"] 63 | 64 | _, exactMatch, regexMatch, requirements := getIngressClasses(httpRequest, proxyTenants) 65 | if len(requirements) > 0 { 66 | ic, errIc := getIngressClassFromRequest(httpRequest) 67 | if errIc != nil { 68 | return nil, errors.NewBadRequest(errIc, g.GroupKind()) 69 | } 70 | 71 | return utils.HandleGetSelector(httpRequest.Context(), ic, g.client, requirements, name, g.GroupKind()) 72 | } 73 | 74 | icl, err := getIngressClassListFromRequest(httpRequest) 75 | if err != nil { 76 | return nil, errors.NewBadRequest(err, g.GroupKind()) 77 | } 78 | 79 | if err = g.client.List(httpRequest.Context(), icl, client.MatchingLabels{corev1.LabelMetadataName: name}); err != nil { 80 | return nil, errors.NewBadRequest(err, g.GroupKind()) 81 | } 82 | 83 | var r *labels.Requirement 84 | 85 | if r, err = getIngressClassSelector(icl, exactMatch, regexMatch); err == nil { 86 | return labels.NewSelector().Add(*r), nil 87 | } 88 | 89 | switch httpRequest.Method { 90 | case http.MethodGet: 91 | return nil, errors.NewNotFoundError(name, g.GroupKind()) 92 | default: 93 | return nil, nil 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /internal/modules/ingressclass/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package ingressclass 5 | 6 | import ( 7 | "github.com/go-logr/logr" 8 | networkingv1 "k8s.io/api/networking/v1" 9 | "k8s.io/apimachinery/pkg/labels" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "k8s.io/apimachinery/pkg/selection" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | 15 | "github.com/projectcapsule/capsule-proxy/internal/modules" 16 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 17 | "github.com/projectcapsule/capsule-proxy/internal/modules/utils" 18 | "github.com/projectcapsule/capsule-proxy/internal/request" 19 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 20 | ) 21 | 22 | type list struct { 23 | client client.Reader 24 | log logr.Logger 25 | gk schema.GroupVersionKind 26 | } 27 | 28 | func List(client client.Reader) modules.Module { 29 | return &list{ 30 | client: client, 31 | log: ctrl.Log.WithName("ingressclass_list"), 32 | gk: schema.GroupVersionKind{ 33 | Group: networkingv1.GroupName, 34 | Version: "*", 35 | Kind: "ingressclasses", 36 | }, 37 | } 38 | } 39 | 40 | func (l list) GroupVersionKind() schema.GroupVersionKind { 41 | return l.gk 42 | } 43 | 44 | func (l list) GroupKind() schema.GroupKind { 45 | return l.gk.GroupKind() 46 | } 47 | 48 | func (l list) Path() string { 49 | return "/apis/networking.k8s.io/{version}/{endpoint:ingressclasses/?}" 50 | } 51 | 52 | func (l list) Methods() []string { 53 | return []string{} 54 | } 55 | 56 | func (l list) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 57 | httpRequest := proxyRequest.GetHTTPRequest() 58 | 59 | allowed, exactMatch, regexMatch, selectorsMatch := getIngressClasses(httpRequest, proxyTenants) 60 | if len(selectorsMatch) > 0 { 61 | return utils.HandleListSelector(selectorsMatch) 62 | } 63 | 64 | icl, err := getIngressClassListFromRequest(httpRequest) 65 | if err != nil { 66 | return nil, errors.NewBadRequest(err, l.GroupKind()) 67 | } 68 | 69 | if err = l.client.List(httpRequest.Context(), icl); err != nil { 70 | return nil, errors.NewBadRequest(err, l.GroupKind()) 71 | } 72 | 73 | var r *labels.Requirement 74 | 75 | if r, err = getIngressClassSelector(icl, exactMatch, regexMatch); err != nil { 76 | if !allowed { 77 | return nil, errors.NewNotAllowed(l.GroupKind()) 78 | } 79 | 80 | r, _ = labels.NewRequirement("dontexistsignoreme", selection.Exists, []string{}) 81 | } 82 | 83 | return labels.NewSelector().Add(*r), nil 84 | } 85 | -------------------------------------------------------------------------------- /internal/modules/lease/get.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package lease 5 | 6 | import ( 7 | "github.com/go-logr/logr" 8 | "github.com/gorilla/mux" 9 | capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" 10 | corev1 "k8s.io/api/core/v1" 11 | "k8s.io/apimachinery/pkg/labels" 12 | "k8s.io/apimachinery/pkg/runtime/schema" 13 | "k8s.io/apimachinery/pkg/types" 14 | ctrl "sigs.k8s.io/controller-runtime" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | 17 | "github.com/projectcapsule/capsule-proxy/internal/modules" 18 | "github.com/projectcapsule/capsule-proxy/internal/request" 19 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 20 | ) 21 | 22 | type get struct { 23 | client client.Reader 24 | log logr.Logger 25 | gk schema.GroupVersionKind 26 | } 27 | 28 | func Get(client client.Reader) modules.Module { 29 | return &get{ 30 | client: client, 31 | log: ctrl.Log.WithName("node_get"), 32 | gk: schema.GroupVersionKind{ 33 | Group: corev1.GroupName, 34 | Version: "*", 35 | Kind: "nodes", 36 | }, 37 | } 38 | } 39 | 40 | func (g get) GroupVersionKind() schema.GroupVersionKind { 41 | return g.gk 42 | } 43 | 44 | func (g get) GroupKind() schema.GroupKind { 45 | return g.gk.GroupKind() 46 | } 47 | 48 | func (g get) Path() string { 49 | return "/apis/coordination.k8s.io/v1/namespaces/kube-node-lease/leases/{name}" 50 | } 51 | 52 | func (g get) Methods() []string { 53 | return []string{"get"} 54 | } 55 | 56 | func (g get) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 57 | var selectors []map[string]string 58 | 59 | httpRequest := proxyRequest.GetHTTPRequest() 60 | 61 | for _, pt := range proxyTenants { 62 | if ok := pt.RequestAllowed(httpRequest, capsulev1beta2.NodesProxy); ok { 63 | selectors = append(selectors, pt.Tenant.Spec.NodeSelector) 64 | } 65 | } 66 | 67 | name := mux.Vars(httpRequest)["name"] 68 | 69 | node := &corev1.Node{} 70 | //nolint:nilerr 71 | if err = g.client.Get(httpRequest.Context(), types.NamespacedName{Name: name}, node); err != nil { 72 | // offload failure to Kubernetes API due to missing RBAC 73 | return nil, nil 74 | } 75 | 76 | for _, sel := range selectors { 77 | for k := range sel { 78 | if sel[k] == node.GetLabels()[k] { 79 | // We're matching the nodeSelector of the Tenant: 80 | // adding an empty selector in order to decorate the request 81 | return labels.NewSelector().Add(), nil 82 | } 83 | } 84 | } 85 | // requesting lease for a non owner Node: let Kubernetes deal with it 86 | return nil, nil 87 | } 88 | -------------------------------------------------------------------------------- /internal/modules/metric/get.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package metric 5 | 6 | import ( 7 | "net/http" 8 | 9 | "github.com/go-logr/logr" 10 | "github.com/gorilla/mux" 11 | corev1 "k8s.io/api/core/v1" 12 | "k8s.io/apimachinery/pkg/labels" 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | ctrl "sigs.k8s.io/controller-runtime" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | 17 | "github.com/projectcapsule/capsule-proxy/internal/modules" 18 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 19 | "github.com/projectcapsule/capsule-proxy/internal/modules/utils" 20 | "github.com/projectcapsule/capsule-proxy/internal/request" 21 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 22 | ) 23 | 24 | type get struct { 25 | client client.Reader 26 | log logr.Logger 27 | gk schema.GroupVersionKind 28 | } 29 | 30 | func Get(client client.Reader) modules.Module { 31 | return &get{ 32 | client: client, 33 | log: ctrl.Log.WithName("metric_get"), 34 | gk: schema.GroupVersionKind{ 35 | Group: "metrics.k8s.io", 36 | Version: "*", 37 | Kind: "nodes", 38 | }, 39 | } 40 | } 41 | 42 | func (g get) GroupVersionKind() schema.GroupVersionKind { 43 | return g.gk 44 | } 45 | 46 | func (g get) GroupKind() schema.GroupKind { 47 | return g.gk.GroupKind() 48 | } 49 | 50 | func (g get) Path() string { 51 | return "/apis/metrics.k8s.io/{version}/nodes/{name}" 52 | } 53 | 54 | func (g get) Methods() []string { 55 | return []string{} 56 | } 57 | 58 | func (g get) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 59 | httpRequest := proxyRequest.GetHTTPRequest() 60 | 61 | selectors := utils.GetNodeSelectors(httpRequest, proxyTenants) 62 | 63 | name := mux.Vars(httpRequest)["name"] 64 | 65 | nl := &corev1.NodeList{} 66 | if err = g.client.List(httpRequest.Context(), nl, client.MatchingLabels{"kubernetes.io/hostname": name}); err != nil { 67 | return nil, errors.NewBadRequest(err, g.GroupKind()) 68 | } 69 | 70 | var r *labels.Requirement 71 | 72 | if r, err = utils.GetNodeSelector(nl, selectors); err == nil { 73 | return labels.NewSelector().Add(*r), nil 74 | } 75 | 76 | if httpRequest.Method == http.MethodGet { 77 | return nil, errors.NewNotFoundError(name, g.GroupKind()) 78 | } 79 | 80 | return nil, nil 81 | } 82 | -------------------------------------------------------------------------------- /internal/modules/metric/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package metric 5 | 6 | import ( 7 | "github.com/go-logr/logr" 8 | corev1 "k8s.io/api/core/v1" 9 | "k8s.io/apimachinery/pkg/labels" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "k8s.io/apimachinery/pkg/selection" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | 15 | "github.com/projectcapsule/capsule-proxy/internal/modules" 16 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 17 | "github.com/projectcapsule/capsule-proxy/internal/modules/utils" 18 | "github.com/projectcapsule/capsule-proxy/internal/request" 19 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 20 | ) 21 | 22 | type list struct { 23 | client client.Reader 24 | log logr.Logger 25 | gk schema.GroupVersionKind 26 | } 27 | 28 | func List(client client.Reader) modules.Module { 29 | return &list{ 30 | client: client, 31 | log: ctrl.Log.WithName("metric_list"), 32 | gk: schema.GroupVersionKind{ 33 | Group: "metrics.k8s.io", 34 | Version: "*", 35 | Kind: "nodes", 36 | }, 37 | } 38 | } 39 | 40 | func (l list) GroupVersionKind() schema.GroupVersionKind { 41 | return l.gk 42 | } 43 | 44 | func (l list) GroupKind() schema.GroupKind { 45 | return l.gk.GroupKind() 46 | } 47 | 48 | func (l list) Path() string { 49 | return "/apis/metrics.k8s.io/{version}/{endpoint:nodes/?}" 50 | } 51 | 52 | func (l list) Methods() []string { 53 | return []string{} 54 | } 55 | 56 | func (l list) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 57 | httpRequest := proxyRequest.GetHTTPRequest() 58 | 59 | selectors := utils.GetNodeSelectors(httpRequest, proxyTenants) 60 | 61 | nl := &corev1.NodeList{} 62 | if err = l.client.List(httpRequest.Context(), nl); err != nil { 63 | return nil, errors.NewBadRequest(err, l.GroupKind()) 64 | } 65 | 66 | var r *labels.Requirement 67 | if r, err = utils.GetNodeSelector(nl, selectors); err != nil { 68 | r, _ = labels.NewRequirement("dontexistsignoreme", selection.Exists, []string{}) 69 | } 70 | 71 | return labels.NewSelector().Add(*r), nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/modules/module.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package modules 5 | 6 | import ( 7 | "k8s.io/apimachinery/pkg/labels" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | 10 | "github.com/projectcapsule/capsule-proxy/internal/request" 11 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 12 | ) 13 | 14 | type Module interface { 15 | GroupVersionKind() schema.GroupVersionKind 16 | GroupKind() schema.GroupKind 17 | Path() string 18 | Methods() []string 19 | Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) 20 | } 21 | -------------------------------------------------------------------------------- /internal/modules/namespace/const.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package namespace 5 | 6 | const ( 7 | basePath = "/api/v1/{endpoint:namespaces/?}" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/modules/namespace/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package namespace 5 | 6 | import ( 7 | "net/http" 8 | 9 | "github.com/go-logr/logr" 10 | corev1 "k8s.io/api/core/v1" 11 | "k8s.io/apimachinery/pkg/labels" 12 | "k8s.io/apimachinery/pkg/runtime/schema" 13 | "k8s.io/apimachinery/pkg/selection" 14 | ctrl "sigs.k8s.io/controller-runtime" 15 | 16 | "github.com/projectcapsule/capsule-proxy/internal/controllers" 17 | "github.com/projectcapsule/capsule-proxy/internal/modules" 18 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 19 | "github.com/projectcapsule/capsule-proxy/internal/request" 20 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 21 | ) 22 | 23 | type list struct { 24 | roleBindingsReflector *controllers.RoleBindingReflector 25 | log logr.Logger 26 | gk schema.GroupVersionKind 27 | } 28 | 29 | func List(roleBindingsReflector *controllers.RoleBindingReflector) modules.Module { 30 | return &list{ 31 | roleBindingsReflector: roleBindingsReflector, 32 | log: ctrl.Log.WithName("namespace_list"), 33 | gk: schema.GroupVersionKind{ 34 | Group: corev1.GroupName, 35 | Version: "*", 36 | Kind: "namespaces", 37 | }, 38 | } 39 | } 40 | 41 | func (l list) GroupVersionKind() schema.GroupVersionKind { 42 | return l.gk 43 | } 44 | 45 | func (l list) GroupKind() schema.GroupKind { 46 | return l.gk.GroupKind() 47 | } 48 | 49 | func (l list) Path() string { 50 | return basePath 51 | } 52 | 53 | func (l list) Methods() []string { 54 | return []string{http.MethodGet} 55 | } 56 | 57 | func (l list) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 58 | var userNamespaces []string 59 | 60 | if l.roleBindingsReflector != nil { 61 | userNamespaces, err = l.roleBindingsReflector.GetUserNamespacesFromRequest(proxyRequest) 62 | if err != nil { 63 | return nil, errors.NewBadRequest(err, l.GroupKind()) 64 | } 65 | } else { 66 | for _, tnt := range proxyTenants { 67 | userNamespaces = append(userNamespaces, tnt.Tenant.Status.Namespaces...) 68 | } 69 | } 70 | 71 | var r *labels.Requirement 72 | 73 | switch { 74 | case len(userNamespaces) > 0: 75 | r, err = labels.NewRequirement(corev1.LabelMetadataName, selection.In, userNamespaces) 76 | default: 77 | r, err = labels.NewRequirement("dontexistsignoreme", selection.Exists, []string{}) 78 | } 79 | 80 | if err != nil { 81 | return nil, errors.NewBadRequest(err, l.GroupKind()) 82 | } 83 | 84 | return labels.NewSelector().Add(*r), nil 85 | } 86 | -------------------------------------------------------------------------------- /internal/modules/namespace/post.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package namespace 5 | 6 | import ( 7 | "net/http" 8 | 9 | "k8s.io/apimachinery/pkg/labels" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | 12 | "github.com/projectcapsule/capsule-proxy/internal/modules" 13 | "github.com/projectcapsule/capsule-proxy/internal/request" 14 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 15 | ) 16 | 17 | type post struct{} 18 | 19 | func Post() modules.Module { 20 | return &post{} 21 | } 22 | 23 | func (l post) GroupVersionKind() schema.GroupVersionKind { 24 | return schema.GroupVersionKind{} 25 | } 26 | 27 | func (l post) GroupKind() schema.GroupKind { 28 | return schema.GroupKind{} 29 | } 30 | 31 | func (l post) Path() string { 32 | return basePath 33 | } 34 | 35 | func (l post) Methods() []string { 36 | return []string{http.MethodPost, http.MethodPut, http.MethodPatch} 37 | } 38 | 39 | func (l post) Handle([]*tenant.ProxyTenant, request.Request) (selector labels.Selector, err error) { 40 | return nil, nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/modules/namespaced/catchall.go: -------------------------------------------------------------------------------- 1 | package namespaced 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | v1 "k8s.io/api/authorization/v1" 8 | "k8s.io/apimachinery/pkg/labels" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | "k8s.io/apimachinery/pkg/selection" 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | 13 | capsulelabels "github.com/projectcapsule/capsule-proxy/internal/labels" 14 | "github.com/projectcapsule/capsule-proxy/internal/modules" 15 | "github.com/projectcapsule/capsule-proxy/internal/request" 16 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 17 | ) 18 | 19 | type catchall struct { 20 | path string 21 | reader client.Reader 22 | writer client.Writer 23 | } 24 | 25 | func CatchAll(client client.Reader, writer client.Writer, path string) modules.Module { 26 | return &catchall{ 27 | path: path, 28 | reader: client, 29 | writer: writer, 30 | } 31 | } 32 | 33 | func (l catchall) GroupVersionKind() schema.GroupVersionKind { 34 | return schema.GroupVersionKind{} 35 | } 36 | 37 | func (l catchall) GroupKind() schema.GroupKind { 38 | return schema.GroupKind{} 39 | } 40 | 41 | func (l catchall) Path() string { 42 | return l.path 43 | } 44 | 45 | func (l catchall) Methods() []string { 46 | return []string{"get"} 47 | } 48 | 49 | func (l catchall) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 50 | user, groups, _ := proxyRequest.GetUserAndGroups() 51 | 52 | var group, version, kind string 53 | 54 | url := proxyRequest.GetHTTPRequest().URL.Path 55 | 56 | parts := strings.Split(url, "/") 57 | 58 | switch len(parts) { 59 | case 5: 60 | group = parts[2] 61 | version = parts[3] 62 | kind = parts[4] 63 | case 4: 64 | version = parts[2] 65 | kind = parts[3] 66 | } 67 | 68 | var sourceTenants []string 69 | 70 | for _, tnt := range proxyTenants { 71 | var allowed bool 72 | 73 | for _, ns := range tnt.Tenant.Status.Namespaces { 74 | sar := v1.SubjectAccessReview{} 75 | sar.Spec.User = user 76 | sar.Spec.Groups = groups 77 | sar.Spec.ResourceAttributes = &v1.ResourceAttributes{ 78 | Namespace: ns, 79 | Verb: "list", 80 | Group: group, 81 | Version: version, 82 | Resource: kind, 83 | } 84 | 85 | if err = l.writer.Create(proxyRequest.GetHTTPRequest().Context(), &sar); err != nil { 86 | return nil, fmt.Errorf("unable to check if user can list %s/%s", group, kind) 87 | } 88 | 89 | allowed = sar.Status.Allowed 90 | 91 | break 92 | } 93 | 94 | if allowed { 95 | sourceTenants = append(sourceTenants, tnt.Tenant.Name) 96 | } 97 | } 98 | 99 | var r *labels.Requirement 100 | 101 | switch { 102 | case len(sourceTenants) > 0: 103 | r, err = labels.NewRequirement(capsulelabels.ManagedByCapsuleLabel, selection.In, sourceTenants) 104 | default: 105 | r, err = labels.NewRequirement("dontexistsignoreme", selection.Exists, []string{}) 106 | } 107 | 108 | return labels.NewSelector().Add(*r), err 109 | } 110 | -------------------------------------------------------------------------------- /internal/modules/node/get.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package node 5 | 6 | import ( 7 | "net/http" 8 | 9 | "github.com/go-logr/logr" 10 | "github.com/gorilla/mux" 11 | corev1 "k8s.io/api/core/v1" 12 | "k8s.io/apimachinery/pkg/labels" 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | ctrl "sigs.k8s.io/controller-runtime" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | 17 | "github.com/projectcapsule/capsule-proxy/internal/modules" 18 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 19 | "github.com/projectcapsule/capsule-proxy/internal/modules/utils" 20 | "github.com/projectcapsule/capsule-proxy/internal/request" 21 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 22 | ) 23 | 24 | type get struct { 25 | client client.Reader 26 | log logr.Logger 27 | gk schema.GroupVersionKind 28 | } 29 | 30 | func Get(client client.Reader) modules.Module { 31 | return &get{ 32 | client: client, 33 | log: ctrl.Log.WithName("node_get"), 34 | gk: schema.GroupVersionKind{ 35 | Group: corev1.GroupName, 36 | Version: "*", 37 | Kind: "nodes", 38 | }, 39 | } 40 | } 41 | 42 | func (g get) GroupVersionKind() schema.GroupVersionKind { 43 | return g.gk 44 | } 45 | 46 | func (g get) GroupKind() schema.GroupKind { 47 | return g.gk.GroupKind() 48 | } 49 | 50 | func (g get) Path() string { 51 | return "/api/v1/nodes/{name}" 52 | } 53 | 54 | func (g get) Methods() []string { 55 | return []string{} 56 | } 57 | 58 | func (g get) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 59 | httpRequest := proxyRequest.GetHTTPRequest() 60 | selectors := utils.GetNodeSelectors(httpRequest, proxyTenants) 61 | 62 | name := mux.Vars(httpRequest)["name"] 63 | 64 | nl := &corev1.NodeList{} 65 | if err = g.client.List(httpRequest.Context(), nl, client.MatchingLabels{"kubernetes.io/hostname": name}); err != nil { 66 | return nil, errors.NewBadRequest(err, g.GroupKind()) 67 | } 68 | 69 | var r *labels.Requirement 70 | 71 | if r, err = utils.GetNodeSelector(nl, selectors); err == nil { 72 | return labels.NewSelector().Add(*r), nil 73 | } 74 | 75 | if httpRequest.Method == http.MethodGet { 76 | return nil, errors.NewNotFoundError(name, g.GroupKind()) 77 | } 78 | 79 | return nil, nil 80 | } 81 | -------------------------------------------------------------------------------- /internal/modules/node/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package node 5 | 6 | import ( 7 | "github.com/go-logr/logr" 8 | corev1 "k8s.io/api/core/v1" 9 | "k8s.io/apimachinery/pkg/labels" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "k8s.io/apimachinery/pkg/selection" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | 15 | "github.com/projectcapsule/capsule-proxy/internal/modules" 16 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 17 | "github.com/projectcapsule/capsule-proxy/internal/modules/utils" 18 | "github.com/projectcapsule/capsule-proxy/internal/request" 19 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 20 | ) 21 | 22 | type list struct { 23 | client client.Reader 24 | log logr.Logger 25 | gk schema.GroupVersionKind 26 | } 27 | 28 | func List(client client.Reader) modules.Module { 29 | return &list{ 30 | client: client, 31 | log: ctrl.Log.WithName("node_list"), 32 | gk: schema.GroupVersionKind{ 33 | Group: corev1.GroupName, 34 | Version: "*", 35 | Kind: "nodes", 36 | }, 37 | } 38 | } 39 | 40 | func (l list) GroupVersionKind() schema.GroupVersionKind { 41 | return l.gk 42 | } 43 | 44 | func (l list) GroupKind() schema.GroupKind { 45 | return l.gk.GroupKind() 46 | } 47 | 48 | func (l list) Path() string { 49 | return "/api/v1/{endpoint:nodes/?}" 50 | } 51 | 52 | func (l list) Methods() []string { 53 | return []string{} 54 | } 55 | 56 | func (l list) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 57 | httpRequest := proxyRequest.GetHTTPRequest() 58 | selectors := utils.GetNodeSelectors(httpRequest, proxyTenants) 59 | 60 | nl := &corev1.NodeList{} 61 | if err = l.client.List(httpRequest.Context(), nl); err != nil { 62 | return nil, errors.NewBadRequest(err, l.GroupKind()) 63 | } 64 | 65 | var r *labels.Requirement 66 | if r, err = utils.GetNodeSelector(nl, selectors); err != nil { 67 | r, _ = labels.NewRequirement("dontexistsignoreme", selection.Exists, []string{}) 68 | } 69 | 70 | return labels.NewSelector().Add(*r), nil 71 | } 72 | -------------------------------------------------------------------------------- /internal/modules/persistentvolume/get.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package persistentvolume 5 | 6 | import ( 7 | "github.com/go-logr/logr" 8 | "github.com/gorilla/mux" 9 | capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" 10 | corev1 "k8s.io/api/core/v1" 11 | "k8s.io/apimachinery/pkg/labels" 12 | "k8s.io/apimachinery/pkg/runtime/schema" 13 | ctrl "sigs.k8s.io/controller-runtime" 14 | "sigs.k8s.io/controller-runtime/pkg/client" 15 | 16 | "github.com/projectcapsule/capsule-proxy/internal/modules" 17 | "github.com/projectcapsule/capsule-proxy/internal/modules/utils" 18 | "github.com/projectcapsule/capsule-proxy/internal/request" 19 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 20 | ) 21 | 22 | type get struct { 23 | client client.Reader 24 | log logr.Logger 25 | labelKey string 26 | gk schema.GroupVersionKind 27 | } 28 | 29 | func Get(client client.Reader) modules.Module { 30 | label, _ := capsulev1beta2.GetTypeLabel(&capsulev1beta2.Tenant{}) 31 | 32 | return &get{ 33 | client: client, 34 | log: ctrl.Log.WithName("persistentvolume_get"), 35 | labelKey: label, 36 | gk: schema.GroupVersionKind{ 37 | Group: corev1.GroupName, 38 | Version: "*", 39 | Kind: "persistentvolumes", 40 | }, 41 | } 42 | } 43 | 44 | func (g get) GroupVersionKind() schema.GroupVersionKind { 45 | return g.gk 46 | } 47 | 48 | func (g get) GroupKind() schema.GroupKind { 49 | return g.gk.GroupKind() 50 | } 51 | 52 | func (g get) Path() string { 53 | return "/api/v1/{endpoint:persistentvolumes}/{name}" 54 | } 55 | 56 | func (g get) Methods() []string { 57 | return []string{} 58 | } 59 | 60 | func (g get) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 61 | httpRequest := proxyRequest.GetHTTPRequest() 62 | 63 | name := mux.Vars(httpRequest)["name"] 64 | 65 | _, requirement := getPersistentVolume(httpRequest, proxyTenants, g.labelKey) 66 | 67 | rc := &corev1.PersistentVolume{} 68 | 69 | return utils.HandleGetSelector(httpRequest.Context(), rc, g.client, []labels.Requirement{requirement}, name, g.GroupKind()) 70 | } 71 | -------------------------------------------------------------------------------- /internal/modules/persistentvolume/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package persistentvolume 5 | 6 | import ( 7 | "github.com/go-logr/logr" 8 | capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" 9 | corev1 "k8s.io/api/core/v1" 10 | "k8s.io/apimachinery/pkg/labels" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | 15 | "github.com/projectcapsule/capsule-proxy/internal/modules" 16 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 17 | "github.com/projectcapsule/capsule-proxy/internal/modules/utils" 18 | "github.com/projectcapsule/capsule-proxy/internal/request" 19 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 20 | ) 21 | 22 | type list struct { 23 | client client.Reader 24 | log logr.Logger 25 | labelKey string 26 | gk schema.GroupVersionKind 27 | } 28 | 29 | func List(client client.Reader) modules.Module { 30 | label, _ := capsulev1beta2.GetTypeLabel(&capsulev1beta2.Tenant{}) 31 | 32 | return &list{ 33 | client: client, 34 | log: ctrl.Log.WithName("persistentvolume_list"), 35 | labelKey: label, 36 | gk: schema.GroupVersionKind{ 37 | Group: corev1.GroupName, 38 | Version: "*", 39 | Kind: "persistentvolumes", 40 | }, 41 | } 42 | } 43 | 44 | func (l list) GroupVersionKind() schema.GroupVersionKind { 45 | return l.gk 46 | } 47 | 48 | func (l list) GroupKind() schema.GroupKind { 49 | return l.gk.GroupKind() 50 | } 51 | 52 | func (l list) Path() string { 53 | return "/api/v1/{endpoint:persistentvolumes/?}" 54 | } 55 | 56 | func (l list) Methods() []string { 57 | return []string{} 58 | } 59 | 60 | func (l list) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 61 | httpRequest := proxyRequest.GetHTTPRequest() 62 | 63 | allowed, requirement := getPersistentVolume(httpRequest, proxyTenants, l.labelKey) 64 | if !allowed { 65 | return nil, errors.NewNotAllowed(l.GroupKind()) 66 | } 67 | 68 | return utils.HandleListSelector([]labels.Requirement{requirement}) 69 | } 70 | -------------------------------------------------------------------------------- /internal/modules/persistentvolume/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package persistentvolume 5 | 6 | import ( 7 | "net/http" 8 | 9 | capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" 10 | "k8s.io/apimachinery/pkg/labels" 11 | "k8s.io/apimachinery/pkg/selection" 12 | 13 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 14 | ) 15 | 16 | func getPersistentVolume(req *http.Request, proxyTenants []*tenant.ProxyTenant, label string) (allowed bool, requirements labels.Requirement) { 17 | var tenantNames []string 18 | 19 | for _, pt := range proxyTenants { 20 | if ok := pt.RequestAllowed(req, capsulev1beta2.PersistentVolumesProxy); ok { 21 | allowed = true 22 | 23 | tenantNames = append(tenantNames, pt.Tenant.Name) 24 | } 25 | } 26 | 27 | requirement, _ := labels.NewRequirement(label, selection.In, tenantNames) 28 | 29 | return allowed, *requirement 30 | } 31 | -------------------------------------------------------------------------------- /internal/modules/priorityclass/get.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package priorityclass 5 | 6 | import ( 7 | "net/http" 8 | 9 | "github.com/go-logr/logr" 10 | "github.com/gorilla/mux" 11 | corev1 "k8s.io/api/core/v1" 12 | schedulingv1 "k8s.io/api/scheduling/v1" 13 | "k8s.io/apimachinery/pkg/labels" 14 | "k8s.io/apimachinery/pkg/runtime/schema" 15 | ctrl "sigs.k8s.io/controller-runtime" 16 | "sigs.k8s.io/controller-runtime/pkg/client" 17 | 18 | "github.com/projectcapsule/capsule-proxy/internal/modules" 19 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 20 | "github.com/projectcapsule/capsule-proxy/internal/modules/utils" 21 | "github.com/projectcapsule/capsule-proxy/internal/request" 22 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 23 | ) 24 | 25 | type get struct { 26 | client client.Reader 27 | log logr.Logger 28 | gk schema.GroupVersionKind 29 | } 30 | 31 | func Get(client client.Reader) modules.Module { 32 | return &get{ 33 | client: client, 34 | log: ctrl.Log.WithName("priorityclass_get"), 35 | gk: schema.GroupVersionKind{ 36 | Group: schedulingv1.GroupName, 37 | Version: "*", 38 | Kind: "priorityclasses", 39 | }, 40 | } 41 | } 42 | 43 | func (g get) GroupVersionKind() schema.GroupVersionKind { 44 | return g.gk 45 | } 46 | 47 | func (g get) GroupKind() schema.GroupKind { 48 | return g.gk.GroupKind() 49 | } 50 | 51 | func (g get) Path() string { 52 | return "/apis/scheduling.k8s.io/v1/{endpoint:priorityclasses}/{name}" 53 | } 54 | 55 | func (g get) Methods() []string { 56 | return []string{} 57 | } 58 | 59 | func (g get) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 60 | httpRequest := proxyRequest.GetHTTPRequest() 61 | 62 | name := mux.Vars(httpRequest)["name"] 63 | 64 | _, exactMatch, regexMatch, requirements := getPriorityClass(httpRequest, proxyTenants) 65 | if len(requirements) > 0 { 66 | pc := &schedulingv1.PriorityClass{} 67 | 68 | return utils.HandleGetSelector(httpRequest.Context(), pc, g.client, requirements, name, g.GroupKind()) 69 | } 70 | 71 | sc := &schedulingv1.PriorityClassList{} 72 | if err = g.client.List(httpRequest.Context(), sc, client.MatchingLabels{corev1.LabelMetadataName: name}); err != nil { 73 | return nil, errors.NewBadRequest(err, g.GroupKind()) 74 | } 75 | 76 | var r *labels.Requirement 77 | r, err = getPriorityClassSelector(sc, exactMatch, regexMatch) 78 | 79 | switch { 80 | case err == nil: 81 | return labels.NewSelector().Add(*r), nil 82 | case httpRequest.Method == http.MethodGet: 83 | return nil, errors.NewNotFoundError(name, g.GroupKind()) 84 | default: 85 | return nil, nil 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/modules/priorityclass/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package priorityclass 5 | 6 | import ( 7 | "github.com/go-logr/logr" 8 | schedulingv1 "k8s.io/api/scheduling/v1" 9 | "k8s.io/apimachinery/pkg/labels" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "k8s.io/apimachinery/pkg/selection" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | 15 | "github.com/projectcapsule/capsule-proxy/internal/modules" 16 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 17 | "github.com/projectcapsule/capsule-proxy/internal/modules/utils" 18 | "github.com/projectcapsule/capsule-proxy/internal/request" 19 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 20 | ) 21 | 22 | type list struct { 23 | client client.Reader 24 | log logr.Logger 25 | gk schema.GroupVersionKind 26 | } 27 | 28 | func List(client client.Reader) modules.Module { 29 | return &list{ 30 | client: client, 31 | log: ctrl.Log.WithName("priorityclass_list"), 32 | gk: schema.GroupVersionKind{ 33 | Group: schedulingv1.GroupName, 34 | Version: "*", 35 | Kind: "priorityclasses", 36 | }, 37 | } 38 | } 39 | 40 | func (l list) GroupVersionKind() schema.GroupVersionKind { 41 | return l.gk 42 | } 43 | 44 | func (l list) GroupKind() schema.GroupKind { 45 | return l.gk.GroupKind() 46 | } 47 | 48 | func (l list) Path() string { 49 | return "/apis/scheduling.k8s.io/v1/{endpoint:priorityclasses/?}" 50 | } 51 | 52 | func (l list) Methods() []string { 53 | return []string{} 54 | } 55 | 56 | func (l list) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 57 | httpRequest := proxyRequest.GetHTTPRequest() 58 | 59 | allowed, exactMatch, regexMatch, selectorsMatch := getPriorityClass(httpRequest, proxyTenants) 60 | if len(selectorsMatch) > 0 { 61 | return utils.HandleListSelector(selectorsMatch) 62 | } 63 | 64 | // Regex Deprecated, Therefor handeled last 65 | sc := &schedulingv1.PriorityClassList{} 66 | if err = l.client.List(httpRequest.Context(), sc); err != nil { 67 | return nil, errors.NewBadRequest(err, l.GroupKind()) 68 | } 69 | 70 | var r *labels.Requirement 71 | 72 | if r, err = getPriorityClassSelector(sc, exactMatch, regexMatch); err != nil { 73 | if !allowed { 74 | return nil, errors.NewNotAllowed(l.GroupKind()) 75 | } 76 | 77 | r, _ = labels.NewRequirement("dontexistsignoreme", selection.Exists, []string{}) 78 | } 79 | 80 | return labels.NewSelector().Add(*r), nil 81 | } 82 | -------------------------------------------------------------------------------- /internal/modules/priorityclass/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package priorityclass 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | "regexp" 10 | "sort" 11 | 12 | capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" 13 | corev1 "k8s.io/api/core/v1" 14 | schedulingv1 "k8s.io/api/scheduling/v1" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/apimachinery/pkg/labels" 17 | "k8s.io/apimachinery/pkg/selection" 18 | 19 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 20 | ) 21 | 22 | func getPriorityClass(req *http.Request, proxyTenants []*tenant.ProxyTenant) (allowed bool, exact []string, regex []*regexp.Regexp, requirements []labels.Requirement) { 23 | requirements = []labels.Requirement{} 24 | 25 | for _, pt := range proxyTenants { 26 | if ok := pt.RequestAllowed(req, capsulev1beta2.PriorityClassesProxy); !ok { 27 | continue 28 | } 29 | 30 | allowed = true 31 | 32 | pc := pt.Tenant.Spec.PriorityClasses 33 | if pc == nil { 34 | continue 35 | } 36 | 37 | if len(pc.SelectorAllowedListSpec.Exact) > 0 { 38 | exact = append(exact, pc.SelectorAllowedListSpec.Exact...) 39 | } 40 | 41 | if len(pc.Default) > 0 { 42 | exact = append(exact, pc.Default) 43 | } 44 | 45 | if r := pc.SelectorAllowedListSpec.Regex; len(r) > 0 { 46 | regex = append(regex, regexp.MustCompile(r)) 47 | } 48 | 49 | selector, err := metav1.LabelSelectorAsSelector(&pc.SelectorAllowedListSpec.LabelSelector) 50 | if err != nil { 51 | continue 52 | } 53 | 54 | reqs, selectable := selector.Requirements() 55 | if !selectable { 56 | continue 57 | } 58 | 59 | requirements = append(requirements, reqs...) 60 | } 61 | 62 | sort.SliceStable(exact, func(i, _ int) bool { 63 | return exact[i] < exact[0] 64 | }) 65 | 66 | return allowed, exact, regex, requirements 67 | } 68 | 69 | func getPriorityClassSelector(classes *schedulingv1.PriorityClassList, exact []string, regex []*regexp.Regexp) (*labels.Requirement, error) { 70 | isPriorityClassRegexed := func(name string, regex []*regexp.Regexp) bool { 71 | for _, r := range regex { 72 | if r.MatchString(name) { 73 | return true 74 | } 75 | } 76 | 77 | return false 78 | } 79 | 80 | var names []string 81 | 82 | for _, s := range classes.Items { 83 | if isPriorityClassRegexed(s.GetName(), regex) { 84 | names = append(names, s.GetName()) 85 | 86 | continue 87 | } 88 | 89 | if f := sort.SearchStrings(exact, s.GetName()); f < len(exact) && exact[f] == s.GetName() { 90 | names = append(names, s.GetName()) 91 | } 92 | } 93 | 94 | if len(names) > 0 { 95 | return labels.NewRequirement(corev1.LabelMetadataName, selection.In, names) 96 | } 97 | 98 | return nil, fmt.Errorf("cannot create LabelSelector for the requested PriorityClass requirement") 99 | } 100 | -------------------------------------------------------------------------------- /internal/modules/runtimeclass/get.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package runtimeclass 5 | 6 | import ( 7 | "github.com/go-logr/logr" 8 | "github.com/gorilla/mux" 9 | nodev1 "k8s.io/api/node/v1" 10 | "k8s.io/apimachinery/pkg/labels" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | 15 | "github.com/projectcapsule/capsule-proxy/internal/modules" 16 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 17 | "github.com/projectcapsule/capsule-proxy/internal/modules/utils" 18 | "github.com/projectcapsule/capsule-proxy/internal/request" 19 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 20 | ) 21 | 22 | type get struct { 23 | client client.Reader 24 | log logr.Logger 25 | gk schema.GroupVersionKind 26 | } 27 | 28 | func Get(client client.Reader) modules.Module { 29 | return &get{ 30 | client: client, 31 | log: ctrl.Log.WithName("runtimeclass_get"), 32 | gk: schema.GroupVersionKind{ 33 | Group: nodev1.GroupName, 34 | Version: "*", 35 | Kind: "runtimeclasses", 36 | }, 37 | } 38 | } 39 | 40 | func (g get) GroupVersionKind() schema.GroupVersionKind { 41 | return g.gk 42 | } 43 | 44 | func (g get) GroupKind() schema.GroupKind { 45 | return g.gk.GroupKind() 46 | } 47 | 48 | func (g get) Path() string { 49 | return "/apis/node.k8s.io/v1/{endpoint:runtimeclasses}/{name}" 50 | } 51 | 52 | func (g get) Methods() []string { 53 | return []string{} 54 | } 55 | 56 | func (g get) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 57 | httpRequest := proxyRequest.GetHTTPRequest() 58 | 59 | name := mux.Vars(httpRequest)["name"] 60 | 61 | _, requirements := getRuntimeClass(httpRequest, proxyTenants) 62 | if len(requirements) == 0 { 63 | return nil, errors.NewNotFoundError(name, g.GroupKind()) 64 | } 65 | 66 | rc := &nodev1.RuntimeClass{} 67 | 68 | return utils.HandleGetSelector(httpRequest.Context(), rc, g.client, requirements, name, g.GroupKind()) 69 | } 70 | -------------------------------------------------------------------------------- /internal/modules/runtimeclass/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package runtimeclass 5 | 6 | import ( 7 | "github.com/go-logr/logr" 8 | nodev1 "k8s.io/api/node/v1" 9 | "k8s.io/apimachinery/pkg/labels" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "k8s.io/apimachinery/pkg/selection" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | 15 | "github.com/projectcapsule/capsule-proxy/internal/modules" 16 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 17 | "github.com/projectcapsule/capsule-proxy/internal/modules/utils" 18 | "github.com/projectcapsule/capsule-proxy/internal/request" 19 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 20 | ) 21 | 22 | type list struct { 23 | client client.Reader 24 | log logr.Logger 25 | gk schema.GroupVersionKind 26 | } 27 | 28 | func List(client client.Reader) modules.Module { 29 | return &list{ 30 | client: client, 31 | log: ctrl.Log.WithName("runtimeclass_list"), 32 | gk: schema.GroupVersionKind{ 33 | Group: nodev1.GroupName, 34 | Version: "*", 35 | Kind: "runtimeclasses", 36 | }, 37 | } 38 | } 39 | 40 | func (l list) GroupVersionKind() schema.GroupVersionKind { 41 | return l.gk 42 | } 43 | 44 | func (l list) GroupKind() schema.GroupKind { 45 | return l.gk.GroupKind() 46 | } 47 | 48 | func (l list) Path() string { 49 | return "/apis/node.k8s.io/v1/{endpoint:runtimeclasses/?}" 50 | } 51 | 52 | func (l list) Methods() []string { 53 | return []string{} 54 | } 55 | 56 | func (l list) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 57 | httpRequest := proxyRequest.GetHTTPRequest() 58 | 59 | allowed, selectorsMatch := getRuntimeClass(httpRequest, proxyTenants) 60 | 61 | if !allowed { 62 | return nil, errors.NewNotAllowed(l.GroupKind()) 63 | } 64 | 65 | if len(selectorsMatch) == 0 { 66 | r, _ := labels.NewRequirement("dontexistsignoreme", selection.Exists, []string{}) 67 | 68 | return labels.NewSelector().Add(*r), nil 69 | } 70 | 71 | return utils.HandleListSelector(selectorsMatch) 72 | } 73 | -------------------------------------------------------------------------------- /internal/modules/runtimeclass/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package runtimeclass 5 | 6 | import ( 7 | "net/http" 8 | 9 | capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/labels" 12 | 13 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 14 | ) 15 | 16 | func getRuntimeClass(req *http.Request, proxyTenants []*tenant.ProxyTenant) (allowed bool, requirements []labels.Requirement) { 17 | requirements = []labels.Requirement{} 18 | 19 | for _, pt := range proxyTenants { 20 | if ok := pt.RequestAllowed(req, capsulev1beta2.RuntimeClassesProxy); ok { 21 | allowed = true 22 | 23 | rc := pt.Tenant.Spec.RuntimeClasses 24 | if rc == nil { 25 | continue 26 | } 27 | 28 | selector, err := metav1.LabelSelectorAsSelector(&rc.LabelSelector) 29 | if err != nil { 30 | continue 31 | } 32 | 33 | reqs, selectable := selector.Requirements() 34 | if !selectable { 35 | continue 36 | } 37 | 38 | requirements = append(requirements, reqs...) 39 | } 40 | } 41 | 42 | return allowed, requirements 43 | } 44 | -------------------------------------------------------------------------------- /internal/modules/storageclass/get.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package storageclass 5 | 6 | import ( 7 | "net/http" 8 | 9 | "github.com/go-logr/logr" 10 | "github.com/gorilla/mux" 11 | corev1 "k8s.io/api/core/v1" 12 | storagev1 "k8s.io/api/storage/v1" 13 | "k8s.io/apimachinery/pkg/labels" 14 | "k8s.io/apimachinery/pkg/runtime/schema" 15 | ctrl "sigs.k8s.io/controller-runtime" 16 | "sigs.k8s.io/controller-runtime/pkg/client" 17 | 18 | "github.com/projectcapsule/capsule-proxy/internal/modules" 19 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 20 | "github.com/projectcapsule/capsule-proxy/internal/modules/utils" 21 | "github.com/projectcapsule/capsule-proxy/internal/request" 22 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 23 | ) 24 | 25 | type get struct { 26 | client client.Reader 27 | log logr.Logger 28 | gk schema.GroupVersionKind 29 | } 30 | 31 | func Get(client client.Reader) modules.Module { 32 | return &get{ 33 | client: client, 34 | log: ctrl.Log.WithName("storageclass_get"), 35 | gk: schema.GroupVersionKind{ 36 | Group: storagev1.GroupName, 37 | Version: "*", 38 | Kind: "storageclasses", 39 | }, 40 | } 41 | } 42 | 43 | func (g get) GroupVersionKind() schema.GroupVersionKind { 44 | return g.gk 45 | } 46 | 47 | func (g get) GroupKind() schema.GroupKind { 48 | return g.gk.GroupKind() 49 | } 50 | 51 | func (g get) Path() string { 52 | return "/apis/storage.k8s.io/v1/{endpoint:storageclasses}/{name}" 53 | } 54 | 55 | func (g get) Methods() []string { 56 | return []string{} 57 | } 58 | 59 | func (g get) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 60 | httpRequest := proxyRequest.GetHTTPRequest() 61 | 62 | name := mux.Vars(httpRequest)["name"] 63 | 64 | _, exactMatch, regexMatch, requirements := getStorageClasses(httpRequest, proxyTenants) 65 | if len(requirements) > 0 { 66 | sc := &storagev1.StorageClass{} 67 | 68 | return utils.HandleGetSelector(httpRequest.Context(), sc, g.client, requirements, name, g.GroupKind()) 69 | } 70 | 71 | sc := &storagev1.StorageClassList{} 72 | if err = g.client.List(httpRequest.Context(), sc, client.MatchingLabels{corev1.LabelMetadataName: name}); err != nil { 73 | return nil, errors.NewBadRequest(err, g.GroupKind()) 74 | } 75 | 76 | var r *labels.Requirement 77 | r, err = getStorageClassSelector(sc, exactMatch, regexMatch) 78 | 79 | switch { 80 | case err == nil: 81 | return labels.NewSelector().Add(*r), nil 82 | case httpRequest.Method == http.MethodGet: 83 | return nil, errors.NewNotFoundError(name, g.GroupKind()) 84 | default: 85 | return nil, nil 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/modules/storageclass/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package storageclass 5 | 6 | import ( 7 | "github.com/go-logr/logr" 8 | storagev1 "k8s.io/api/storage/v1" 9 | "k8s.io/apimachinery/pkg/labels" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "k8s.io/apimachinery/pkg/selection" 12 | ctrl "sigs.k8s.io/controller-runtime" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | 15 | "github.com/projectcapsule/capsule-proxy/internal/modules" 16 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 17 | "github.com/projectcapsule/capsule-proxy/internal/modules/utils" 18 | "github.com/projectcapsule/capsule-proxy/internal/request" 19 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 20 | ) 21 | 22 | type list struct { 23 | client client.Reader 24 | log logr.Logger 25 | gk schema.GroupVersionKind 26 | } 27 | 28 | func List(client client.Reader) modules.Module { 29 | return &list{ 30 | client: client, 31 | log: ctrl.Log.WithName("storageclass_list"), 32 | gk: schema.GroupVersionKind{ 33 | Group: storagev1.GroupName, 34 | Version: "*", 35 | Kind: "storageclasses", 36 | }, 37 | } 38 | } 39 | 40 | func (l list) GroupVersionKind() schema.GroupVersionKind { 41 | return l.gk 42 | } 43 | 44 | func (l list) GroupKind() schema.GroupKind { 45 | return l.gk.GroupKind() 46 | } 47 | 48 | func (l list) Path() string { 49 | return "/apis/storage.k8s.io/v1/{endpoint:storageclasses/?}" 50 | } 51 | 52 | func (l list) Methods() []string { 53 | return []string{} 54 | } 55 | 56 | func (l list) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 57 | httpRequest := proxyRequest.GetHTTPRequest() 58 | 59 | allowed, exactMatch, regexMatch, selectorsMatch := getStorageClasses(httpRequest, proxyTenants) 60 | if len(selectorsMatch) > 0 { 61 | return utils.HandleListSelector(selectorsMatch) 62 | } 63 | 64 | sc := &storagev1.StorageClassList{} 65 | if err = l.client.List(httpRequest.Context(), sc); err != nil { 66 | return nil, errors.NewBadRequest(err, l.GroupKind()) 67 | } 68 | 69 | var r *labels.Requirement 70 | 71 | if r, err = getStorageClassSelector(sc, exactMatch, regexMatch); err != nil { 72 | if !allowed { 73 | return nil, errors.NewNotAllowed(l.GroupKind()) 74 | } 75 | 76 | r, _ = labels.NewRequirement("dontexistsignoreme", selection.Exists, []string{}) 77 | } 78 | 79 | return labels.NewSelector().Add(*r), nil 80 | } 81 | -------------------------------------------------------------------------------- /internal/modules/storageclass/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package storageclass 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | "regexp" 10 | "sort" 11 | 12 | capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" 13 | corev1 "k8s.io/api/core/v1" 14 | storagev1 "k8s.io/api/storage/v1" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/apimachinery/pkg/labels" 17 | "k8s.io/apimachinery/pkg/selection" 18 | 19 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 20 | ) 21 | 22 | func getStorageClasses(req *http.Request, proxyTenants []*tenant.ProxyTenant) (allowed bool, exact []string, regex []*regexp.Regexp, requirements []labels.Requirement) { 23 | requirements = []labels.Requirement{} 24 | 25 | for _, pt := range proxyTenants { 26 | if ok := pt.RequestAllowed(req, capsulev1beta2.StorageClassesProxy); !ok { 27 | continue 28 | } 29 | 30 | allowed = true 31 | 32 | sc := pt.Tenant.Spec.StorageClasses 33 | if sc == nil { 34 | continue 35 | } 36 | 37 | if len(sc.SelectorAllowedListSpec.Exact) > 0 { 38 | exact = append(exact, sc.SelectorAllowedListSpec.Exact...) 39 | } 40 | 41 | if len(sc.Default) > 0 { 42 | exact = append(exact, sc.Default) 43 | } 44 | 45 | if r := sc.SelectorAllowedListSpec.Regex; len(r) > 0 { 46 | regex = append(regex, regexp.MustCompile(r)) 47 | } 48 | 49 | selector, err := metav1.LabelSelectorAsSelector(&sc.SelectorAllowedListSpec.LabelSelector) 50 | if err != nil { 51 | continue 52 | } 53 | 54 | reqs, selectable := selector.Requirements() 55 | if !selectable { 56 | continue 57 | } 58 | 59 | requirements = append(requirements, reqs...) 60 | } 61 | 62 | sort.SliceStable(exact, func(i, _ int) bool { 63 | return exact[i] < exact[0] 64 | }) 65 | 66 | return allowed, exact, regex, requirements 67 | } 68 | 69 | func getStorageClassSelector(classes *storagev1.StorageClassList, exact []string, regex []*regexp.Regexp) (*labels.Requirement, error) { 70 | isStorageClassRegexed := func(name string, regex []*regexp.Regexp) bool { 71 | for _, r := range regex { 72 | if r.MatchString(name) { 73 | return true 74 | } 75 | } 76 | 77 | return false 78 | } 79 | 80 | var names []string 81 | 82 | for _, s := range classes.Items { 83 | if isStorageClassRegexed(s.GetName(), regex) { 84 | names = append(names, s.GetName()) 85 | 86 | continue 87 | } 88 | 89 | if f := sort.SearchStrings(exact, s.GetName()); f < len(exact) && exact[f] == s.GetName() { 90 | names = append(names, s.GetName()) 91 | } 92 | } 93 | 94 | if len(names) > 0 { 95 | return labels.NewRequirement(corev1.LabelMetadataName, selection.In, names) 96 | } 97 | 98 | return nil, fmt.Errorf("cannot create LabelSelector for the requested StorageClass requirement") 99 | } 100 | -------------------------------------------------------------------------------- /internal/modules/tenants/const.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tenants 5 | 6 | const ( 7 | basePath = "/apis/capsule.clastix.io/v1beta2/{endpoint:tenants/?}" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/modules/tenants/get.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tenants 5 | 6 | import ( 7 | "net/http" 8 | 9 | "github.com/go-logr/logr" 10 | "github.com/gorilla/mux" 11 | capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" 12 | "k8s.io/apimachinery/pkg/labels" 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | "k8s.io/apimachinery/pkg/util/sets" 15 | ctrl "sigs.k8s.io/controller-runtime" 16 | "sigs.k8s.io/controller-runtime/pkg/client" 17 | 18 | "github.com/projectcapsule/capsule-proxy/internal/modules" 19 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 20 | "github.com/projectcapsule/capsule-proxy/internal/request" 21 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 22 | ) 23 | 24 | type get struct { 25 | capsuleLabel string 26 | client client.Reader 27 | log logr.Logger 28 | gk schema.GroupVersionKind 29 | } 30 | 31 | func Get(client client.Reader) modules.Module { 32 | label, _ := capsulev1beta2.GetTypeLabel(&capsulev1beta2.Tenant{}) 33 | 34 | return &get{ 35 | capsuleLabel: label, 36 | client: client, 37 | log: ctrl.Log.WithName("tenant_get"), 38 | gk: schema.GroupVersionKind{ 39 | Group: "capsule.clastix.io", 40 | Version: "*", 41 | Kind: "tenants", 42 | }, 43 | } 44 | } 45 | 46 | func (g get) GroupVersionKind() schema.GroupVersionKind { 47 | return g.gk 48 | } 49 | 50 | func (g get) GroupKind() schema.GroupKind { 51 | return g.gk.GroupKind() 52 | } 53 | 54 | func (g get) Path() string { 55 | return "/apis/{}/v1beta2/tenants/{name}" 56 | } 57 | 58 | func (g get) Methods() []string { 59 | return []string{http.MethodGet} 60 | } 61 | 62 | func (g get) Handle(proxyTenants []*tenant.ProxyTenant, proxyRequest request.Request) (selector labels.Selector, err error) { 63 | name := mux.Vars(proxyRequest.GetHTTPRequest())["name"] 64 | 65 | userTenants := sets.New[string]() 66 | 67 | for _, tnt := range proxyTenants { 68 | userTenants.Insert(tnt.Tenant.Name) 69 | } 70 | 71 | if userTenants.Has(name) { 72 | return labels.NewSelector(), nil 73 | } 74 | 75 | return nil, errors.NewNotFoundError(name, g.GroupKind()) 76 | } 77 | -------------------------------------------------------------------------------- /internal/modules/tenants/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tenants 5 | 6 | import ( 7 | "net/http" 8 | 9 | "github.com/go-logr/logr" 10 | "k8s.io/apimachinery/pkg/labels" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | "k8s.io/apimachinery/pkg/selection" 13 | ctrl "sigs.k8s.io/controller-runtime" 14 | 15 | "github.com/projectcapsule/capsule-proxy/internal/modules" 16 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 17 | "github.com/projectcapsule/capsule-proxy/internal/request" 18 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 19 | ) 20 | 21 | type list struct { 22 | log logr.Logger 23 | gk schema.GroupVersionKind 24 | } 25 | 26 | func List() modules.Module { 27 | return &list{ 28 | log: ctrl.Log.WithName("tenant_list"), 29 | gk: schema.GroupVersionKind{ 30 | Group: "capsule.clastix.io", 31 | Version: "*", 32 | Kind: "tenants", 33 | }, 34 | } 35 | } 36 | 37 | func (l list) GroupVersionKind() schema.GroupVersionKind { 38 | return l.gk 39 | } 40 | 41 | func (l list) GroupKind() schema.GroupKind { 42 | return l.gk.GroupKind() 43 | } 44 | 45 | func (l list) Path() string { 46 | return basePath 47 | } 48 | 49 | func (l list) Methods() []string { 50 | return []string{http.MethodGet} 51 | } 52 | 53 | func (l list) Handle(proxyTenants []*tenant.ProxyTenant, _ request.Request) (selector labels.Selector, err error) { 54 | userTenants := make([]string, 0, len(proxyTenants)) 55 | 56 | for _, tnt := range proxyTenants { 57 | userTenants = append(userTenants, tnt.Tenant.Name) 58 | } 59 | 60 | var r *labels.Requirement 61 | 62 | switch { 63 | case len(userTenants) > 0: 64 | r, err = labels.NewRequirement("kubernetes.io/metadata.name", selection.In, userTenants) 65 | default: 66 | r, err = labels.NewRequirement("dontexistsignoreme", selection.Exists, []string{}) 67 | } 68 | 69 | if err != nil { 70 | return nil, errors.NewBadRequest(err, l.GroupKind()) 71 | } 72 | 73 | return labels.NewSelector().Add(*r), nil 74 | } 75 | -------------------------------------------------------------------------------- /internal/modules/utils/clusterscope.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/labels" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | 11 | v1beta1 "github.com/projectcapsule/capsule-proxy/api/v1beta1" 12 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 13 | ) 14 | 15 | // Calculate Requirements for a given GroupVersionKind based on the ProxyTenants clusterResource configurations. 16 | func GetClusterScopeRequirements(gvk *schema.GroupVersionKind, proxyTenants []*tenant.ProxyTenant) (operations []v1beta1.ClusterResourceOperation, requirements []labels.Requirement) { 17 | operations = []v1beta1.ClusterResourceOperation{} 18 | requirements = []labels.Requirement{} 19 | 20 | for _, pt := range proxyTenants { 21 | for _, cr := range pt.ClusterResources { 22 | if matchResource(gvk, cr) { 23 | // Append Operations 24 | operations = append(operations, cr.Operations...) 25 | 26 | // Append Selector 27 | selector, err := metav1.LabelSelectorAsSelector(cr.Selector) 28 | if err != nil { 29 | continue 30 | } 31 | 32 | reqs, selectable := selector.Requirements() 33 | if !selectable { 34 | continue 35 | } 36 | 37 | requirements = append(requirements, reqs...) 38 | } 39 | } 40 | } 41 | 42 | return operations, requirements 43 | } 44 | 45 | func matchResource(gvk *schema.GroupVersionKind, cr v1beta1.ClusterResource) (match bool) { 46 | kindMatch := false 47 | groupVersionMatch := false 48 | 49 | for _, resource := range cr.Resources { 50 | if resource == "*" { 51 | kindMatch = true 52 | 53 | break 54 | } 55 | 56 | if gvk.Kind == resource { 57 | kindMatch = true 58 | 59 | break 60 | } 61 | } 62 | 63 | if !kindMatch { 64 | return match 65 | } 66 | 67 | // Check if the group/version matches any of the apiGroups using regex 68 | for _, apiGroup := range cr.APIGroups { 69 | // Handle wildcard "*" to match any group 70 | if apiGroup == "*" { 71 | groupVersionMatch = true 72 | 73 | break 74 | } 75 | 76 | // Replace "*" with ".*" for regex compatibility and ensure match against the entire string 77 | regexPattern := "^" + regexp.QuoteMeta(apiGroup) + "$" 78 | regexPattern = strings.ReplaceAll(regexPattern, "\\*", ".*") 79 | 80 | matched, _ := regexp.MatchString(regexPattern, gvk.Group+"/"+gvk.Version) 81 | if matched { 82 | groupVersionMatch = true 83 | 84 | break 85 | } 86 | } 87 | 88 | if kindMatch && groupVersionMatch { 89 | match = true 90 | } 91 | 92 | return match 93 | } 94 | -------------------------------------------------------------------------------- /internal/modules/utils/gvk.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "k8s.io/client-go/discovery" 9 | ) 10 | 11 | // GetGVKByPlural returns the GroupVersionKind for a given plural name. 12 | func ReplacePluralWithKind(discoveryClient *discovery.DiscoveryClient, gvk *schema.GroupVersionKind) error { 13 | resourceList, err := discoveryClient.ServerResourcesForGroupVersion(gvk.Group + "/" + gvk.Version) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | for _, resource := range resourceList.APIResources { 19 | if resource.Name == gvk.Kind { 20 | gvk.Kind = resource.Kind 21 | 22 | return nil 23 | } 24 | } 25 | 26 | return fmt.Errorf("could not find GVK for plural name: %s", gvk.Kind) 27 | } 28 | 29 | // Since the URL is in the format /apis/{group}/{version}/{kind} or /api/{version}/{kind}, we can extract the GVK from the URL. 30 | // However the kind will be the plural form. 31 | func GetGVKFromURL(url string) *schema.GroupVersionKind { 32 | parts := strings.Split(url, "/") 33 | 34 | switch len(parts) { 35 | case 5, 6: 36 | return &schema.GroupVersionKind{ 37 | Group: parts[2], 38 | Version: parts[3], 39 | Kind: parts[4], 40 | } 41 | case 4: 42 | return &schema.GroupVersionKind{ 43 | Version: parts[2], 44 | Kind: parts[3], 45 | } 46 | } 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/modules/utils/node.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package utils 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | 10 | capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" 11 | corev1 "k8s.io/api/core/v1" 12 | "k8s.io/apimachinery/pkg/labels" 13 | "k8s.io/apimachinery/pkg/selection" 14 | 15 | "github.com/projectcapsule/capsule-proxy/internal/tenant" 16 | ) 17 | 18 | func GetNodeSelector(nl *corev1.NodeList, selectors []map[string]string) (*labels.Requirement, error) { 19 | var names []string 20 | 21 | for _, node := range nl.Items { 22 | for _, selector := range selectors { 23 | matches := 0 24 | 25 | for k := range selector { 26 | if selector[k] == node.GetLabels()[k] { 27 | matches++ 28 | } 29 | } 30 | 31 | if matches == len(selector) { 32 | names = append(names, node.GetName()) 33 | } 34 | } 35 | } 36 | 37 | if len(names) > 0 { 38 | return labels.NewRequirement("kubernetes.io/hostname", selection.In, names) 39 | } 40 | 41 | return nil, fmt.Errorf("cannot create LabelSelector for the requested Node requirement") 42 | } 43 | 44 | func GetNodeSelectors(request *http.Request, proxyTenants []*tenant.ProxyTenant) (selectors []map[string]string) { 45 | for _, pt := range proxyTenants { 46 | if ok := pt.RequestAllowed(request, capsulev1beta2.NodesProxy); ok { 47 | selectors = append(selectors, pt.Tenant.Spec.NodeSelector) 48 | } 49 | } 50 | 51 | return 52 | } 53 | -------------------------------------------------------------------------------- /internal/modules/utils/selector.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package utils 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "strings" 10 | 11 | apierrors "k8s.io/apimachinery/pkg/api/errors" 12 | "k8s.io/apimachinery/pkg/labels" 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | "k8s.io/apimachinery/pkg/selection" 15 | "k8s.io/apimachinery/pkg/types" 16 | "k8s.io/apimachinery/pkg/util/sets" 17 | "sigs.k8s.io/controller-runtime/pkg/client" 18 | 19 | "github.com/projectcapsule/capsule-proxy/internal/modules/errors" 20 | ) 21 | 22 | func HandleGetSelector(ctx context.Context, obj client.Object, client client.Reader, requirements []labels.Requirement, name string, gv schema.GroupKind) (labels.Selector, error) { 23 | if err := client.Get(ctx, types.NamespacedName{Name: name}, obj); err != nil { 24 | if apierrors.IsNotFound(err) { 25 | return nil, errors.NewNotFoundError(name, gv) 26 | } 27 | 28 | return nil, err 29 | } 30 | 31 | selector := labels.NewSelector() 32 | 33 | for _, requirement := range requirements { 34 | if requirement.Matches(labels.Set(obj.GetLabels())) { 35 | return selector.Add(requirement), nil 36 | } 37 | } 38 | 39 | return nil, errors.NewNotFoundError(name, gv) 40 | } 41 | 42 | func HandleListSelector(requirements []labels.Requirement) (selector labels.Selector, err error) { 43 | selector = labels.NewSelector() 44 | 45 | requirementsMap := make(map[string]sets.Set[string]) 46 | generateRequirementsKey := func(requirement labels.Requirement) string { //nolint:nolintlint 47 | switch requirement.Operator() { //nolint:exhaustive 48 | case selection.Equals, selection.DoubleEquals, selection.In: 49 | return fmt.Sprintf("%s:%s", requirement.Key(), selection.In) 50 | case selection.NotEquals, selection.NotIn: 51 | return fmt.Sprintf("%s:%s", requirement.Key(), selection.NotIn) 52 | default: 53 | return fmt.Sprintf("%s:%s", requirement.Key(), requirement.Operator()) 54 | } 55 | } 56 | 57 | for _, requirement := range requirements { 58 | key := generateRequirementsKey(requirement) 59 | 60 | if _, ok := requirementsMap[key]; !ok { 61 | requirementsMap[key] = sets.Set[string](requirement.Values()) 62 | 63 | continue 64 | } 65 | 66 | requirementsMap[key] = requirementsMap[key].Union(sets.Set[string](requirement.Values())) 67 | } 68 | 69 | for k, v := range requirementsMap { 70 | key, op := strings.Split(k, ":")[0], strings.Split(k, ":")[1] 71 | 72 | requirement, err := labels.NewRequirement(key, selection.Operator(op), v.UnsortedList()) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | selector = selector.Add(*requirement) 78 | } 79 | 80 | return selector, nil 81 | } 82 | -------------------------------------------------------------------------------- /internal/options/http.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package options 5 | 6 | import ( 7 | "crypto/x509" 8 | "fmt" 9 | "os" 10 | 11 | "k8s.io/client-go/rest" 12 | "k8s.io/client-go/util/cert" 13 | ) 14 | 15 | type httpOptions struct { 16 | isTLS bool 17 | port uint 18 | crtPath string 19 | keyPath string 20 | caPool *x509.CertPool 21 | } 22 | 23 | func NewServer(isTLS bool, port uint, crtPath string, keyPath string, config *rest.Config) (ServerOptions, error) { 24 | var err error 25 | 26 | if isTLS { 27 | if _, err = os.Stat(crtPath); err != nil { 28 | return nil, fmt.Errorf("cannot lookup TLS certificate file: %w", err) 29 | } 30 | 31 | if _, err = os.Stat(keyPath); err != nil { 32 | return nil, fmt.Errorf("cannot lookup TLS certificate key file: %w", err) 33 | } 34 | } 35 | 36 | var caPool *x509.CertPool 37 | 38 | if caPool, err = cert.NewPool(config.CAFile); err != nil { 39 | if caPool, err = cert.NewPoolFromBytes(config.CAData); err != nil { 40 | return nil, fmt.Errorf("cannot find any CA data, nor from file nor from kubeconfig: %w", err) 41 | } 42 | } 43 | 44 | return &httpOptions{isTLS: isTLS, port: port, crtPath: crtPath, keyPath: keyPath, caPool: caPool}, nil 45 | } 46 | 47 | func (h httpOptions) GetCertificateAuthorityPool() *x509.CertPool { 48 | return h.caPool 49 | } 50 | 51 | func (h httpOptions) IsListeningTLS() bool { 52 | return h.isTLS 53 | } 54 | 55 | func (h httpOptions) ListeningPort() uint { 56 | return h.port 57 | } 58 | 59 | func (h httpOptions) TLSCertificatePath() string { 60 | return h.crtPath 61 | } 62 | 63 | func (h httpOptions) TLSCertificateKeyPath() string { 64 | return h.keyPath 65 | } 66 | -------------------------------------------------------------------------------- /internal/options/listener.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package options 5 | 6 | import ( 7 | "net/http" 8 | "net/url" 9 | "regexp" 10 | 11 | "github.com/projectcapsule/capsule-proxy/internal/request" 12 | ) 13 | 14 | type ListenerOpts interface { 15 | AuthTypes() []request.AuthType 16 | KubernetesControlPlaneURL() *url.URL 17 | IgnoredGroupNames() []string 18 | IgnoredImpersonationsGroups() []string 19 | ImpersonationGroupsRegexp() *regexp.Regexp 20 | PreferredUsernameClaim() string 21 | ReverseProxyTransport() (*http.Transport, error) 22 | BearerTokenFile() string 23 | BearerToken() string 24 | SkipImpersonationReview() bool 25 | } 26 | -------------------------------------------------------------------------------- /internal/options/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package options 5 | 6 | import ( 7 | "crypto/x509" 8 | ) 9 | 10 | type ServerOptions interface { 11 | IsListeningTLS() bool 12 | ListeningPort() uint 13 | TLSCertificatePath() string 14 | TLSCertificateKeyPath() string 15 | GetCertificateAuthorityPool() *x509.CertPool 16 | } 17 | -------------------------------------------------------------------------------- /internal/request/authtype.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | //go:generate stringer -type AuthType 4 | 5 | type AuthType int 6 | 7 | const ( 8 | BearerToken AuthType = iota 9 | TLSCertificate 10 | Anonymous 11 | ) 12 | -------------------------------------------------------------------------------- /internal/request/authtype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type AuthType"; DO NOT EDIT. 2 | 3 | package request 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[BearerToken-0] 12 | _ = x[TLSCertificate-1] 13 | _ = x[Anonymous-2] 14 | } 15 | 16 | const _AuthType_name = "BearerTokenTLSCertificateAnonymous" 17 | 18 | var _AuthType_index = [...]uint8{0, 11, 25, 34} 19 | 20 | func (i AuthType) String() string { 21 | if i < 0 || i >= AuthType(len(_AuthType_index)-1) { 22 | return "AuthType(" + strconv.FormatInt(int64(i), 10) + ")" 23 | } 24 | return _AuthType_name[_AuthType_index[i]:_AuthType_index[i+1]] 25 | } 26 | -------------------------------------------------------------------------------- /internal/request/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package request 5 | 6 | //nolint:errname 7 | type ErrUnauthorized struct { 8 | message string 9 | } 10 | 11 | func NewErrUnauthorized(message string) *ErrUnauthorized { 12 | return &ErrUnauthorized{ 13 | message: message, 14 | } 15 | } 16 | 17 | func (e *ErrUnauthorized) Error() string { 18 | return e.message 19 | } 20 | -------------------------------------------------------------------------------- /internal/request/impersonation.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package request 5 | 6 | import ( 7 | nethttp "net/http" 8 | "regexp" 9 | "strings" 10 | 11 | authenticationv1 "k8s.io/api/authentication/v1" 12 | ) 13 | 14 | func SanitizeImpersonationHeaders(request *nethttp.Request) { 15 | request.Header.Del(authenticationv1.ImpersonateUserHeader) 16 | request.Header.Del(authenticationv1.ImpersonateGroupHeader) 17 | 18 | for header := range request.Header { 19 | if strings.HasPrefix(header, authenticationv1.ImpersonateUserExtraHeaderPrefix) { 20 | request.Header.Del(header) 21 | } 22 | } 23 | } 24 | 25 | func GetImpersonatingUser(request *nethttp.Request) string { 26 | return request.Header.Get(authenticationv1.ImpersonateUserHeader) 27 | } 28 | 29 | func GetImpersonatingGroups(request *nethttp.Request, ignoreImpersonationGroups []string, impersonationGroupsRegexp *regexp.Regexp) []string { 30 | groups := request.Header.Values(authenticationv1.ImpersonateGroupHeader) 31 | if len(groups) > 0 { 32 | if impersonationGroupsRegexp != nil { 33 | groups = filterGroups(groups, impersonationGroupsRegexp) 34 | } 35 | 36 | if len(ignoreImpersonationGroups) > 0 { 37 | groups = ignoreGroups(groups, regexp.MustCompile(strings.Join(ignoreImpersonationGroups, "|"))) 38 | } 39 | } 40 | 41 | return groups 42 | } 43 | 44 | func filterGroups(groups []string, impersonationGroupsRegexp *regexp.Regexp) []string { 45 | filteredGroups := []string{} 46 | 47 | for _, group := range groups { 48 | if impersonationGroupsRegexp.MatchString(group) { 49 | filteredGroups = append(filteredGroups, group) 50 | } 51 | } 52 | 53 | return filteredGroups 54 | } 55 | 56 | func ignoreGroups(groups []string, ignoredGroupsRegexp *regexp.Regexp) []string { 57 | ignoredGroups := []string{} 58 | 59 | for _, group := range groups { 60 | if !ignoredGroupsRegexp.MatchString(group) { 61 | // If the group does NOT match the regex, include it in the filtered list 62 | ignoredGroups = append(ignoredGroups, group) 63 | } 64 | } 65 | 66 | return ignoredGroups 67 | } 68 | -------------------------------------------------------------------------------- /internal/request/request.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package request 5 | 6 | import ( 7 | h "net/http" 8 | ) 9 | 10 | type Request interface { 11 | GetUserAndGroups() (string, []string, error) 12 | GetHTTPRequest() *h.Request 13 | } 14 | -------------------------------------------------------------------------------- /internal/tenant/operations.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tenant 5 | 6 | import ( 7 | "net/http" 8 | 9 | capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" 10 | ) 11 | 12 | type Operations struct { 13 | List bool 14 | Update bool 15 | Delete bool 16 | } 17 | 18 | func defaultOperations() *Operations { 19 | return &Operations{ 20 | List: false, 21 | Update: false, 22 | Delete: false, 23 | } 24 | } 25 | 26 | func (o *Operations) Allow(operation capsulev1beta2.ProxyOperation) { 27 | switch operation { 28 | case capsulev1beta2.ListOperation: 29 | o.List = true 30 | case capsulev1beta2.UpdateOperation: 31 | o.Update = true 32 | case capsulev1beta2.DeleteOperation: 33 | o.Delete = true 34 | } 35 | } 36 | 37 | func (o *Operations) IsAllowed(request *http.Request) (ok bool) { 38 | switch request.Method { 39 | case http.MethodGet: 40 | ok = o.List 41 | case http.MethodPut, http.MethodPatch: 42 | ok = o.List 43 | ok = ok && o.Update 44 | case http.MethodDelete: 45 | ok = o.List 46 | ok = ok && o.Delete 47 | default: 48 | break 49 | } 50 | 51 | return 52 | } 53 | -------------------------------------------------------------------------------- /internal/tenant/proxytenant.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tenant 5 | 6 | import ( 7 | "net/http" 8 | 9 | capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | 12 | "github.com/projectcapsule/capsule-proxy/api/v1beta1" 13 | ) 14 | 15 | type ProxyTenant struct { 16 | Tenant capsulev1beta2.Tenant 17 | ProxySetting map[capsulev1beta2.ProxyServiceKind]*Operations 18 | ClusterResources []v1beta1.ClusterResource 19 | } 20 | 21 | func defaultProxySettings() map[capsulev1beta2.ProxyServiceKind]*Operations { 22 | return map[capsulev1beta2.ProxyServiceKind]*Operations{ 23 | capsulev1beta2.NodesProxy: defaultOperations(), 24 | capsulev1beta2.StorageClassesProxy: defaultOperations(), 25 | capsulev1beta2.IngressClassesProxy: defaultOperations(), 26 | capsulev1beta2.PriorityClassesProxy: defaultOperations(), 27 | capsulev1beta2.RuntimeClassesProxy: defaultOperations(), 28 | capsulev1beta2.PersistentVolumesProxy: defaultOperations(), 29 | } 30 | } 31 | 32 | func NewProxyTenant(ownerName string, ownerKind capsulev1beta2.OwnerKind, tenant capsulev1beta2.Tenant, owners []v1beta1.OwnerSpec) *ProxyTenant { 33 | var ( 34 | tenantProxySettings []capsulev1beta2.ProxySettings 35 | tenantClusterResources []v1beta1.ClusterResource 36 | ) 37 | 38 | for _, owner := range owners { 39 | if owner.Name == ownerName && owner.Kind == ownerKind { 40 | tenantProxySettings = owner.ProxyOperations 41 | tenantClusterResources = owner.ClusterResources 42 | } 43 | } 44 | 45 | proxySettings := defaultProxySettings() 46 | 47 | for _, setting := range tenantProxySettings { 48 | for _, operation := range setting.Operations { 49 | proxySettings[setting.Kind].Allow(operation) 50 | } 51 | } 52 | 53 | return &ProxyTenant{ 54 | Tenant: tenant, 55 | ProxySetting: proxySettings, 56 | ClusterResources: tenantClusterResources, 57 | } 58 | } 59 | 60 | // This Function returns a ProxyTenant struct for GlobalProxySettings. These Settings are currently not bound to a tenant and therefor 61 | // an empty tenant and empty ProxySettings are returned. 62 | func NewClusterProxy(ownerName string, ownerKind capsulev1beta2.OwnerKind, owners []v1beta1.GlobalSubjectSpec) *ProxyTenant { 63 | var tenantClusterResources []v1beta1.ClusterResource 64 | 65 | for _, global := range owners { 66 | for _, subject := range global.Subjects { 67 | if subject.Name == ownerName && subject.Kind == ownerKind { 68 | tenantClusterResources = global.ClusterResources 69 | } 70 | } 71 | } 72 | 73 | return &ProxyTenant{ 74 | Tenant: capsulev1beta2.Tenant{ 75 | ObjectMeta: metav1.ObjectMeta{ 76 | Name: "global", 77 | }, 78 | Spec: capsulev1beta2.TenantSpec{}, 79 | }, 80 | ProxySetting: defaultProxySettings(), 81 | ClusterResources: tenantClusterResources, 82 | } 83 | } 84 | 85 | func (p *ProxyTenant) RequestAllowed(request *http.Request, serviceKind capsulev1beta2.ProxyServiceKind) (ok bool) { 86 | return p.ProxySetting[serviceKind].IsAllowed(request) 87 | } 88 | -------------------------------------------------------------------------------- /internal/utils/gvk.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | ) 9 | 10 | type ProxyGroupVersionKind struct { 11 | schema.GroupVersionKind 12 | // This must be used for path-based routing by the webserver filter. 13 | URLName string 14 | } 15 | 16 | func (g ProxyGroupVersionKind) Path() string { 17 | var parts []string 18 | 19 | if g.Group != "" { 20 | parts = append(parts, "apis") 21 | parts = append(parts, g.Group) 22 | } else { 23 | parts = append(parts, "api") 24 | } 25 | 26 | parts = append(parts, g.Version) 27 | parts = append(parts, g.URLName) 28 | 29 | return fmt.Sprintf("/%s", strings.Join(parts, "/")) 30 | } 31 | 32 | func (g ProxyGroupVersionKind) ResourcePath() string { 33 | return fmt.Sprintf("%s/{name}", g.Path()) 34 | } 35 | -------------------------------------------------------------------------------- /internal/webhooks/watchdog.go: -------------------------------------------------------------------------------- 1 | package webhooks 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | 8 | "github.com/go-logr/logr" 9 | capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" 10 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 13 | 14 | capsulelabels "github.com/projectcapsule/capsule-proxy/internal/labels" 15 | ) 16 | 17 | // MutatingWebhook handles mutating webhook requests. 18 | type WatchdogWebhook struct { 19 | Decoder admission.Decoder 20 | Client client.Client 21 | Log logr.Logger 22 | } 23 | 24 | // Handle processes the admission request and adds a label if necessary. 25 | func (mw *WatchdogWebhook) Handle(ctx context.Context, req admission.Request) admission.Response { 26 | mw.Log.V(7).Info("Received Request") 27 | // Only consider namespaced objects 28 | if req.Namespace == "" { 29 | return admission.Allowed("not namespaced object") 30 | } 31 | 32 | // Decode the object 33 | obj := &unstructured.Unstructured{} 34 | if err := mw.Decoder.Decode(req, obj); err != nil { 35 | return admission.Errored(http.StatusBadRequest, err) 36 | } 37 | 38 | tntList := capsulev1beta2.TenantList{} 39 | if err := mw.Client.List(ctx, &tntList, client.MatchingFields{".status.namespaces": obj.GetNamespace()}); err != nil { 40 | admission.Errored(http.StatusInternalServerError, err) 41 | } 42 | 43 | if len(tntList.Items) == 0 { 44 | return admission.Allowed("no tenant object") 45 | } 46 | 47 | tenant := tntList.Items[0].Name 48 | 49 | mw.Log.V(7).Info("matching tenant", "name", tenant) 50 | 51 | // Add the label if not present 52 | labels := obj.GetLabels() 53 | if labels == nil { 54 | labels = map[string]string{} 55 | } 56 | 57 | if currentValue, exists := labels[capsulelabels.ManagedByCapsuleLabel]; exists && currentValue == tenant { 58 | mw.Log.V(7).Info("label is already correctly set", capsulelabels.ManagedByCapsuleLabel, currentValue) 59 | 60 | return admission.Allowed("tenant already set correctly") 61 | } 62 | 63 | // Add Label 64 | labels[capsulelabels.ManagedByCapsuleLabel] = tntList.Items[0].Name 65 | obj.SetLabels(labels) 66 | 67 | mw.Log.V(7).Info("added label", capsulelabels.ManagedByCapsuleLabel, tntList.Items[0].Name) 68 | 69 | // Marshal the object back to JSON 70 | marshaledObj, err := json.Marshal(obj) 71 | if err != nil { 72 | return admission.Errored(http.StatusInternalServerError, err) 73 | } 74 | 75 | return admission.PatchResponseFromRaw(req.Object.Raw, marshaledObj) 76 | } 77 | -------------------------------------------------------------------------------- /internal/webserver/errors/panic.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package errors 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "net/http" 10 | 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | ) 13 | 14 | func HandleUnauthorized(w http.ResponseWriter, err error, message string) { 15 | message = fmt.Sprintf("%s: %s", message, err.Error()) 16 | status := &metav1.Status{ 17 | TypeMeta: metav1.TypeMeta{ 18 | Kind: "Status", 19 | APIVersion: "v1", 20 | }, 21 | Status: metav1.StatusFailure, 22 | Message: message, 23 | Reason: metav1.StatusReasonForbidden, 24 | Code: http.StatusForbidden, 25 | } 26 | 27 | w.Header().Set("content-type", "application/json") 28 | 29 | //nolint:errchkjson 30 | b, _ := json.Marshal(status) 31 | _, _ = w.Write(b) 32 | 33 | panic(message) 34 | } 35 | 36 | func HandleError(w http.ResponseWriter, err error, message string) { 37 | message = fmt.Sprintf("%s: %s", message, err.Error()) 38 | status := &metav1.Status{ 39 | TypeMeta: metav1.TypeMeta{ 40 | Kind: "Status", 41 | APIVersion: "v1", 42 | }, 43 | Message: message, 44 | Reason: metav1.StatusReasonInternalError, 45 | } 46 | 47 | w.Header().Set("content-type", "application/json") 48 | 49 | //nolint:errchkjson 50 | b, _ := json.Marshal(status) 51 | _, _ = w.Write(b) 52 | 53 | panic(message) 54 | } 55 | -------------------------------------------------------------------------------- /internal/webserver/filter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package webserver 5 | 6 | import ( 7 | "net/http" 8 | 9 | "sigs.k8s.io/controller-runtime/pkg/manager" 10 | ) 11 | 12 | type Filter interface { 13 | manager.Runnable 14 | ReadinessProbe(req *http.Request) error 15 | LivenessProbe(req *http.Request) error 16 | } 17 | -------------------------------------------------------------------------------- /internal/webserver/middleware/allowed_requests.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package middleware 5 | 6 | import ( 7 | "net/http" 8 | 9 | "github.com/go-logr/logr" 10 | "github.com/gorilla/mux" 11 | "k8s.io/apimachinery/pkg/util/sets" 12 | ) 13 | 14 | func CheckPaths(log logr.Logger, allowedPaths sets.Set[string], skipTo func(writer http.ResponseWriter, request *http.Request)) mux.MiddlewareFunc { 15 | return func(next http.Handler) http.Handler { 16 | return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 17 | if allowedPaths.Has(request.URL.Path) { 18 | log.V(4).Info("allowed url path.", "url path", request.URL.Path) 19 | skipTo(writer, request) 20 | 21 | return 22 | } 23 | 24 | next.ServeHTTP(writer, request) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/webserver/middleware/jwt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package middleware 5 | 6 | import ( 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/gorilla/mux" 11 | goerrors "github.com/pkg/errors" 12 | authenticationv1 "k8s.io/api/authentication/v1" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/util/sets" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | 17 | "github.com/projectcapsule/capsule-proxy/internal/webserver/errors" 18 | ) 19 | 20 | func CheckJWTMiddleware(client client.Writer) mux.MiddlewareFunc { 21 | invalidatedToken := sets.New[string]() 22 | 23 | return func(next http.Handler) http.Handler { 24 | return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 25 | var err error 26 | 27 | token := strings.ReplaceAll(request.Header.Get("Authorization"), "Bearer ", "") 28 | 29 | switch { 30 | case len(token) > 0 && !invalidatedToken.Has(token): 31 | tr := authenticationv1.TokenReview{ 32 | TypeMeta: metav1.TypeMeta{ 33 | Kind: "TokenReview", 34 | APIVersion: "authentication.k8s.io/v1", 35 | }, 36 | Spec: authenticationv1.TokenReviewSpec{ 37 | Token: token, 38 | }, 39 | } 40 | if err = client.Create(request.Context(), &tr); err != nil { 41 | errors.HandleError(writer, err, "cannot create TokenReview") 42 | } 43 | 44 | if statusErr := tr.Status.Error; len(statusErr) > 0 { 45 | invalidatedToken.Insert(token) 46 | 47 | errors.HandleUnauthorized(writer, goerrors.New(statusErr), "cannot authenticate the token due to error") 48 | } 49 | case invalidatedToken.Has(token): 50 | errors.HandleUnauthorized(writer, goerrors.New("token is invalid"), "cannot authenticate the token due to error") 51 | } 52 | 53 | next.ServeHTTP(writer, request) 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/webserver/middleware/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Clastix Labs 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package middleware 5 | 6 | import ( 7 | "bufio" 8 | "fmt" 9 | "net" 10 | "net/http" 11 | "strconv" 12 | 13 | "github.com/gorilla/mux" 14 | "github.com/prometheus/client_golang/prometheus" 15 | "github.com/prometheus/client_golang/prometheus/promauto" 16 | "sigs.k8s.io/controller-runtime/pkg/metrics" 17 | ) 18 | 19 | //nolint:gochecknoinits 20 | func init() { 21 | metrics.Registry.MustRegister(totalRequests, httpDuration) 22 | } 23 | 24 | type httpResponseWriter struct { 25 | http.ResponseWriter 26 | statusCode int 27 | } 28 | 29 | func (h *httpResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 30 | hijacker, ok := h.ResponseWriter.(http.Hijacker) 31 | if !ok { 32 | return nil, nil, fmt.Errorf("writer is not http.Hijacker") 33 | } 34 | 35 | return hijacker.Hijack() 36 | } 37 | 38 | func newHTTPResponseWriter(w http.ResponseWriter) *httpResponseWriter { 39 | return &httpResponseWriter{ 40 | w, 41 | http.StatusOK, 42 | } 43 | } 44 | 45 | func (h *httpResponseWriter) WriteHeader(statusCode int) { 46 | h.statusCode = statusCode 47 | h.ResponseWriter.WriteHeader(statusCode) 48 | } 49 | 50 | //nolint:gochecknoglobals 51 | var totalRequests = prometheus.NewCounterVec( 52 | prometheus.CounterOpts{ 53 | Name: "capsule_proxy_requests_total", 54 | Help: "Number of requests", 55 | }, 56 | []string{"path", "status"}, 57 | ) 58 | 59 | //nolint:gochecknoglobals 60 | var httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ 61 | Name: "capsule_proxy_response_time_seconds", 62 | Help: "Duration of capsule proxy requests.", 63 | }, []string{"path"}) 64 | 65 | func MetricsMiddleware(next http.Handler) http.Handler { 66 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 67 | route := mux.CurrentRoute(r) 68 | path, _ := route.GetPathTemplate() 69 | 70 | timer := prometheus.NewTimer(httpDuration.WithLabelValues(path)) 71 | 72 | rw := newHTTPResponseWriter(w) 73 | next.ServeHTTP(rw, r) 74 | 75 | statusCode := rw.statusCode 76 | 77 | totalRequests.WithLabelValues(path, strconv.Itoa(statusCode)).Inc() 78 | 79 | timer.ObserveDuration() 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /internal/webserver/middleware/metrics_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Clastix Labs 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //nolint:testpackage 5 | package middleware 6 | 7 | import ( 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/gorilla/mux" 13 | "github.com/prometheus/client_golang/prometheus" 14 | model "github.com/prometheus/client_model/go" 15 | ) 16 | 17 | func dummyHandler(w http.ResponseWriter, _ *http.Request) { 18 | _, _ = w.Write([]byte("hello")) 19 | w.WriteHeader(http.StatusOK) 20 | } 21 | 22 | func newRequest(method, url string) (*http.Request, error) { 23 | req, err := http.NewRequest(method, url, nil) //nolint:noctx 24 | 25 | return req, err 26 | } 27 | 28 | func Test_MetricsMiddleware_RequestCount(t *testing.T) { 29 | t.Parallel() 30 | 31 | testCases := []struct { 32 | name string 33 | requestCount int 34 | path string 35 | output float64 36 | }{ 37 | { 38 | name: "single request count", 39 | requestCount: 1, 40 | path: "/test", 41 | output: 1, 42 | }, 43 | } 44 | 45 | //nolint:paralleltest 46 | for _, test := range testCases { 47 | router := mux.NewRouter() 48 | router.HandleFunc(test.path, dummyHandler).Methods("GET") 49 | router.Use(MetricsMiddleware) 50 | 51 | rw := httptest.NewRecorder() 52 | 53 | for range test.requestCount { 54 | req, err := newRequest("GET", test.path) 55 | if err != nil { 56 | t.Errorf("failed to create HTTP request object") 57 | } 58 | 59 | router.ServeHTTP(rw, req) 60 | } 61 | 62 | t.Run("regular middleware call", func(t *testing.T) { 63 | ch := make(chan prometheus.Metric) 64 | go totalRequests.Collect(ch) 65 | g := (<-ch).(prometheus.Counter) 66 | result := readVector(g) 67 | if test.output != result.value { 68 | t.Errorf("testcase %s failed. expected: %f, got: %f", test.name, test.output, result.value) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | type metricResult struct { 75 | value float64 76 | labels map[string]string 77 | } 78 | 79 | func labels2Map(labels []*model.LabelPair) map[string]string { 80 | res := map[string]string{} 81 | for _, l := range labels { 82 | res[l.GetName()] = l.GetValue() 83 | } 84 | 85 | return res 86 | } 87 | 88 | func readVector(g prometheus.Metric) metricResult { 89 | m := &model.Metric{} 90 | _ = g.Write(m) 91 | 92 | return metricResult{ 93 | value: m.GetCounter().GetValue(), 94 | labels: labels2Map(m.GetLabel()), 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /internal/webserver/middleware/user_in_group.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2023 Project Capsule Authors. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package middleware 5 | 6 | import ( 7 | "net/http" 8 | "regexp" 9 | 10 | "github.com/go-logr/logr" 11 | "github.com/gorilla/mux" 12 | "k8s.io/apimachinery/pkg/util/sets" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | 15 | "github.com/projectcapsule/capsule-proxy/internal/controllers" 16 | req "github.com/projectcapsule/capsule-proxy/internal/request" 17 | ) 18 | 19 | func CheckUserInIgnoredGroupMiddleware(client client.Writer, log logr.Logger, claim string, authTypes []req.AuthType, ignoredUserGroups sets.Set[string], ignoredImpersonationGroups []string, impersonationGroupsRegexp *regexp.Regexp, skipImpersonationReview bool, fn func(writer http.ResponseWriter, request *http.Request)) mux.MiddlewareFunc { 20 | return func(next http.Handler) http.Handler { 21 | return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 22 | if ignoredUserGroups.Len() > 0 { 23 | user, groups, err := req.NewHTTP(request, authTypes, claim, client, ignoredImpersonationGroups, impersonationGroupsRegexp, skipImpersonationReview).GetUserAndGroups() 24 | if err != nil { 25 | log.Error(err, "Cannot retrieve username and group from request") 26 | } 27 | 28 | for _, group := range groups { 29 | if ignoredUserGroups.Has(group) { 30 | log.V(5).Info("current user belongs to ignored groups", "user", user) 31 | fn(writer, request) 32 | 33 | return 34 | } 35 | } 36 | } 37 | 38 | next.ServeHTTP(writer, request) 39 | }) 40 | } 41 | } 42 | 43 | func CheckUserInCapsuleGroupMiddleware(client client.Writer, log logr.Logger, claim string, authTypes []req.AuthType, ignoredImpersonationGroups []string, impersonationGroupsRegexp *regexp.Regexp, skipImpersonationReview bool, impersonate func(http.ResponseWriter, *http.Request)) mux.MiddlewareFunc { 44 | return func(next http.Handler) http.Handler { 45 | return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 46 | _, groups, err := req.NewHTTP(request, authTypes, claim, client, ignoredImpersonationGroups, impersonationGroupsRegexp, skipImpersonationReview).GetUserAndGroups() 47 | if err != nil { 48 | log.Error(err, "Cannot retrieve username and group from request") 49 | } 50 | 51 | log.V(10).Info("request groups", "groups", groups) 52 | 53 | for _, group := range groups { 54 | if controllers.CapsuleUserGroups.Has(group) { 55 | next.ServeHTTP(writer, request) 56 | 57 | return 58 | } 59 | } 60 | 61 | log.V(5).Info("current user is not a Capsule one", "capsule-groups", controllers.CapsuleUserGroups) 62 | impersonate(writer, request) 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/webserver/utils.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/pkg/errors" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "k8s.io/apimachinery/pkg/util/sets" 9 | "k8s.io/client-go/discovery" 10 | 11 | "github.com/projectcapsule/capsule-proxy/internal/modules" 12 | "github.com/projectcapsule/capsule-proxy/internal/utils" 13 | ) 14 | 15 | func moduleGroupKindPresent(modules []modules.Module, clusterModule utils.ProxyGroupVersionKind) (present bool) { 16 | present = false 17 | 18 | for _, mod := range modules { 19 | if mod.GroupKind().Group == clusterModule.Group && mod.GroupKind().Kind == clusterModule.URLName { 20 | present = true 21 | 22 | break 23 | } 24 | } 25 | 26 | return 27 | } 28 | 29 | func serverPreferredResources(discoveryClient *discovery.DiscoveryClient) (out []utils.ProxyGroupVersionKind, err error) { 30 | apiResourceLists, err := discoveryClient.ServerPreferredResources() 31 | if err != nil { 32 | return nil, errors.Wrap(err, "cannot retrieve server's preferred resources") 33 | } 34 | 35 | for _, ar := range apiResourceLists { 36 | parts := strings.Split(ar.GroupVersion, "/") 37 | 38 | var group, version string 39 | 40 | if len(parts) == 1 { 41 | group = "" 42 | version = ar.GroupVersion 43 | } else { 44 | group = parts[0] 45 | version = parts[1] 46 | } 47 | 48 | for _, i := range ar.APIResources { 49 | // Skip namespaced resources 50 | if i.Namespaced { 51 | continue 52 | } 53 | 54 | if !sets.New[string]([]string(i.Verbs)...).Has("get") { 55 | continue 56 | } 57 | 58 | out = append(out, utils.ProxyGroupVersionKind{ 59 | GroupVersionKind: schema.GroupVersionKind{ 60 | Group: group, 61 | Version: version, 62 | Kind: i.Kind, 63 | }, 64 | URLName: i.Name, 65 | }) 66 | } 67 | } 68 | 69 | return out, nil 70 | } 71 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended", ":dependencyDashboard"], 4 | "baseBranches": ["main"], 5 | "prHourlyLimit": 0, 6 | "prConcurrentLimit": 0, 7 | "branchConcurrentLimit": 0, 8 | "mode": "full", 9 | "commitMessageLowerCase": "auto", 10 | "semanticCommits": "enabled", 11 | "semanticCommitType": "feat", 12 | "ignorePaths": [ 13 | "docs" 14 | ], 15 | "flux": { 16 | "fileMatch": ["^.*flux\\.yaml$"] 17 | }, 18 | "packageRules": [ 19 | { 20 | "matchManagers": ["github-actions", "flux"], 21 | "groupName": "all-ci-updates", 22 | "updateTypes": ["major", "minor", "patch"] 23 | } 24 | ], 25 | "customManagers": [ 26 | { 27 | "customType": "regex", 28 | "fileMatch": ["^Makefile$"], 29 | "matchStrings": [ 30 | "(?[A-Z0-9_]+)_VERSION\\s*[:=?]+\\s*\"?(?[^\"\\r\\n]+)\"?[\\s\\S]*?(?[A-Z0-9_]+)_LOOKUP\\s*[:=?]+\\s*\"?(?[^\"\\r\\n]+)\"?(?:[\\s\\S]*?(?[A-Z0-9_]+)_SOURCE\\s*[:=?]+\\s*\"?(?[^\"\\r\\n]+)\"?)?" 31 | ], 32 | "depNameTemplate": "{{lookupValue}}", 33 | "datasourceTemplate": "{{#sourceValue}}{{sourceValue}}{{/sourceValue}}{{^sourceValue}}github-tags{{/sourceValue}}", 34 | "lookupNameTemplate": "{{lookupValue}}", 35 | "versioningTemplate": "semver" 36 | }, 37 | { 38 | "customType": "regex", 39 | "fileMatch": [".*\\.pre-commit-config\\.ya?ml$"], 40 | "matchStrings": [ 41 | "repo:\\s*https://github\\.com/(?[^/]+/[^\\s]+)[\\s\\S]*?rev:\\s*(?v?\\d+\\.\\d+\\.\\d+)" 42 | ], 43 | "depNameTemplate": "{{lookupValue}}", 44 | "datasourceTemplate": "github-tags", 45 | "lookupNameTemplate": "{{lookupValue}}", 46 | "versioningTemplate": "semver" 47 | } 48 | ] 49 | } 50 | --------------------------------------------------------------------------------