Compare commits

..

62 Commits

Author SHA1 Message Date
Oliver Bähler
2261ea6f4e feat(helm): add labels and annotations for capsuleconfiguration (#1710)
Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2025-10-20 15:37:12 +02:00
renovate[bot]
d1e0ac5be6 chore(deps): update all-ci-updates (#1707)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-18 10:20:25 +03:00
renovate[bot]
ba15a83f94 fix(deps): update k8s.io/utils digest to bc988d5 (#1676)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-16 11:18:38 +03:00
renovate[bot]
40d17bcdba fix(deps): update module github.com/onsi/ginkgo/v2 to v2.26.0 (#1678)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-16 11:18:17 +03:00
renovate[bot]
0863915307 chore(deps): update dependency go to v1.25.3 (#1701)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-16 11:17:58 +03:00
renovate[bot]
97f05c062c chore(deps): update actions/stale digest to 65d1d48 (#1703)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-16 11:17:27 +03:00
renovate[bot]
66d304ab92 chore(deps): update anchore/sbom-action digest to d8a2c01 (#1704)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-16 11:17:10 +03:00
renovate[bot]
5d07cc29a4 chore(deps): update all-ci-updates (#1677)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-16 11:16:44 +03:00
sandert-k8s
deb4db72a1 fix(docs): add static width per logo for adopters (#1700)
Signed-off-by: sandert-k8s <sandert98@gmail.com>
2025-10-13 19:38:42 +02:00
sandert-k8s
51518679f6 chore(docs): Add ODC-Noord as adopter (#1699)
Signed-off-by: sandert-k8s <sandert98@gmail.com>
2025-10-13 12:04:08 +02:00
sandert-k8s
c7b672cde5 chore(docs): sort adopters alphabetically and fix logos (#1698)
Signed-off-by: sandert-k8s <sandert98@gmail.com>
2025-10-13 10:53:54 +02:00
renovate[bot]
e7da3b080a fix(deps): update module sigs.k8s.io/controller-runtime to v0.22.3 (#1697)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-12 15:59:47 +03:00
renovate[bot]
800d49c7f8 chore(deps): update dependency go to v1.25.2 (#1687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-12 15:59:31 +03:00
renovate[bot]
d342fad60f chore(deps): update github/codeql-action digest to 17783bf (#1696)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-12 11:43:22 +02:00
Oliver Bähler
beafe09f71 feat(tenant): allow additional metadata for rolebindings (#1695)
Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2025-10-12 11:42:41 +02:00
Oliver Bähler
ea2b6ec1e3 fix(chart): disable node webhook by default (#1685)
* fix(chart): disable node webhook by default

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix(chart): prevent controller panic for deepequal

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix(chart): no rendering if hostusers if not disabled

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore(enterprise): add e2e suite until 1.30

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: revert e2e

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2025-10-09 19:00:22 +02:00
renovate[bot]
7ccb64dc47 fix(deps): update module sigs.k8s.io/gateway-api to v1.4.0 (#1681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 15:16:16 +02:00
renovate[bot]
e6de39d920 chore(deps): update azure/setup-helm digest to 1a275c3 (#1598)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 15:15:40 +02:00
renovate[bot]
b1d0f8b441 fix(deps): update module sigs.k8s.io/cluster-api to v1.11.2 (#1688)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 15:15:12 +02:00
renovate[bot]
a5e79a43b5 chore(deps): update dependency alessandrojcm/commitlint-pre-commit-hook to v9.23.0 (#1674)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 15:14:44 +02:00
renovate[bot]
89e8da3ac9 chore(deps): update dependency helm/chart-testing to v3.14.0 (#1693)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 15:14:25 +02:00
renovate[bot]
66b3c6971c chore(deps): update github/codeql-action action to v4 (#1689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 15:14:03 +02:00
Dario Tranchitella
1e8cf5dc1f chore: labelling renovate pull requests (#1694)
Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>
2025-10-09 15:03:55 +02:00
renovate[bot]
f8f237d585 chore(deps): update github/codeql-action digest to 6fd4ceb (#1686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 14:08:27 +02:00
Oliver Bähler
c901412df1 feat(api): migrate capsule.clastix.io/managed-by to meta api (#1691)
Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2025-10-08 14:40:01 +02:00
renovate[bot]
d865df2b2b chore(deps): update actions/stale digest to 5f858e3 (#1679)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 15:11:56 +02:00
renovate[bot]
ef83abdfe8 chore(deps): update github/codeql-action digest to 2f11c17 (#1683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 12:23:07 +02:00
renovate[bot]
8254c55848 fix(deps): update module sigs.k8s.io/controller-runtime to v0.22.2 (#1684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 12:22:50 +02:00
Oliver Bähler
14e09ead3c feat: pre-release correctures (#1682)
* chore(metrics): cleanup emitted metrics

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore(ci): bump kind 1.34

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(chart): specific crd names for job rbac

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2025-10-06 19:21:01 +02:00
Oliver Bähler
5ac0f83c5a feat(controller): refactor namespace core loop and state management (#1680)
* feat(controller): allow owners to promote serviceaccounts within tenant as owners

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(controller): refactor status handling for tenants and owned namespaces (including metrics)

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2025-10-06 08:19:26 +02:00
renovate[bot]
9a2effd74e chore(deps): update github/codeql-action digest to 065c6cf (#1675)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-04 23:45:27 +02:00
renovate[bot]
b8f7d5a227 chore(deps): update dependency golangci/golangci-lint to v2.5.0 (#1663)
* chore(deps): update dependency golangci/golangci-lint to v2.5.0

* chore(deps): update dependency golangci/golangci-lint to v2.5.0

Signed-off-by: Hristo Hristov <me@hhristov.info>

* chore(deps): update dependency golangci/golangci-lint to v2.5.0

Signed-off-by: Hristo Hristov <me@hhristov.info>

---------

Signed-off-by: Hristo Hristov <me@hhristov.info>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Hristo Hristov <me@hhristov.info>
2025-10-02 09:45:17 +02:00
renovate[bot]
3b6ac1f377 chore(deps): update amannn/action-semantic-pull-request digest to e49f57c (#1672)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 09:44:37 +02:00
renovate[bot]
e983c51a0a chore(deps): update ossf/scorecard-action action to v2.4.3 (#1671)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 07:32:12 +02:00
renovate[bot]
ef63830907 chore(deps): update github/codeql-action digest to 80cb6b5 (#1670)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 22:25:08 +03:00
Hristo Hristov
4878e1ab1f fix: bypass resourepool limits (#1669)
* fix: bypass resourepool limits

Signed-off-by: Hristo Hristov <me@hhristov.info>

* fix: bypass resourepool limits

Signed-off-by: Hristo Hristov <me@hhristov.info>

---------

Signed-off-by: Hristo Hristov <me@hhristov.info>
2025-09-29 09:39:44 +02:00
renovate[bot]
611a7eba8e chore(deps): update github/codeql-action digest to 6a87ebe (#1661)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-27 23:46:08 +03:00
renovate[bot]
bae5d23ccb chore(deps): update github/codeql-action action to v3.30.5 (#1667)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-27 23:45:47 +03:00
renovate[bot]
9bd18d5f08 chore(deps): update zgosalvez/github-actions-ensure-sha-pinned-actions action to v4 (#1668)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-27 23:45:31 +03:00
renovate[bot]
b88f21478c chore(deps): update all-ci-updates (#1665)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-26 13:45:17 +03:00
renovate[bot]
72a6148896 chore(deps): update securego/gosec action to v2.22.9 (#1664)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-24 00:54:00 +03:00
renovate[bot]
9965b6ce70 chore(deps): update anchore/sbom-action digest to c73dd3f (#1660)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 17:34:51 +03:00
renovate[bot]
bdf34ee026 chore(deps): update github/codeql-action digest to 573acd9 (#1658)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 17:34:34 +03:00
renovate[bot]
d271031b7c chore(deps): update anchore/sbom-action digest to f8bdd1d (#1659)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 19:48:32 +03:00
renovate[bot]
3a6de640bf chore(deps): update dependency go to v1.25.1 (#1580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-14 14:37:31 +03:00
renovate[bot]
7793f5a8a1 chore(deps): update github/codeql-action digest to aa90e97 (#1655)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 21:03:57 +02:00
renovate[bot]
1942dd4835 chore(deps): update sigstore/cosign-installer action to v3.10.0 (#1656)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 21:03:34 +02:00
Oliver Bähler
dd70ac2b9f feat(tenant): owners are now an optional property (#1654)
Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2025-09-12 14:21:10 +02:00
Oliver Bähler
9fa1abac65 feat(controller): allow owners to promote serviceaccounts within tenant as owners (#1626)
* feat(controller): allow owners to promote serviceaccounts within tenant as owners

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: remove harpoon

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2025-09-11 23:12:45 +02:00
renovate[bot]
a2e4e00724 chore(deps): update github/codeql-action digest to 148e76a (#1652)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 22:37:48 +02:00
renovate[bot]
ee5c8f02ed chore(deps): update github/codeql-action digest to 25e54df (#1649)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 12:53:34 +03:00
renovate[bot]
7542ebda5e chore(deps): update github/codeql-action action to v3.30.3 (#1650)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 12:52:26 +03:00
renovate[bot]
e2418ab095 fix(deps): update module sigs.k8s.io/controller-runtime to v0.22.1 (#1620)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 12:52:07 +03:00
renovate[bot]
b9dc782c47 chore(deps): update github/codeql-action digest to 31d3ae8 (#1640)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-10 17:01:09 +03:00
renovate[bot]
d7097b5750 chore(deps): update anchore/sbom-action digest to 039eeb2 (#1645)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-10 17:00:50 +03:00
renovate[bot]
2c210ae4db chore(deps): update github/codeql-action action to v3.30.2 (#1648)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-10 17:00:31 +03:00
renovate[bot]
54e80f8df1 fix(deps): update module github.com/onsi/ginkgo/v2 to v2.25.3 (#1605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-09 17:47:48 +03:00
renovate[bot]
7d617aee47 chore(deps): update github/codeql-action action to v3.30.1 (#1641)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 23:27:55 +03:00
renovate[bot]
bb8a5110ec fix(deps): update module github.com/prometheus/client_golang to v1.23.2 (#1642)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 23:27:36 +03:00
renovate[bot]
6e0cae7185 chore(deps): update codecov/codecov-action action to v5.5.1 (#1638)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 09:42:19 +02:00
renovate[bot]
c65a142e83 fix(deps): update module github.com/prometheus/client_golang to v1.23.1 (#1639)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 09:41:12 +02:00
renovate[bot]
f60e52d633 chore(deps): update github/codeql-action digest to 2d2f57e (#1637)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 09:40:45 +02:00
121 changed files with 3996 additions and 673 deletions

View File

@@ -9,11 +9,11 @@ inputs:
runs:
using: composite
steps:
- uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-pkg-mod-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
- uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
if: ${{ inputs.build-cache-key }}
with:
path: ~/.cache/go-build

View File

@@ -17,7 +17,7 @@ jobs:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Ensure SHA pinned actions
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@fc87bb5b5a97953d987372e74478de634726b3e5 # v3.0.25
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@9e9574ef04ea69da568d6249bd69539ccc704e74 # v4.0.0
with:
# slsa-github-generator requires using a semver tag for reusable workflows.
# See: https://github.com/slsa-framework/slsa-github-generator#referencing-slsa-builders-and-generators

View File

@@ -15,7 +15,7 @@ jobs:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@e7d011b07ef37e089bea6539210f6a0d360d8af9
- uses: amannn/action-semantic-pull-request@e49f57ce06c1747542fce2243c7a98682384bc0e
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

View File

@@ -52,11 +52,11 @@ jobs:
with:
go-version-file: 'go.mod'
- name: Run Gosec Security Scanner
uses: securego/gosec@c9453023c4e81ebdb6dde29e22d9cd5e2285fb16 # v2.22.8
uses: securego/gosec@6be2b51fd78feca86af91f5186b7964d76cb1256 # v2.22.10
with:
args: '-no-fail -fmt sarif -out gosec.sarif ./...'
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@1fd8a71a1271a5ad7639a423fdd9485e1be64031
uses: github/codeql-action/upload-sarif@17783bfb99b07f70fae080b654aed0c514057477
with:
sarif_file: gosec.sarif
unit_tests:
@@ -77,7 +77,7 @@ jobs:
value: ${{ secrets.CODECOV_TOKEN }}
- name: Upload Report to Codecov
if: ${{ steps.checksecret.outputs.result == 'true' }}
uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: projectcapsule/capsule

View File

@@ -40,6 +40,6 @@ jobs:
# See: https://github.com/aquasecurity/trivy-action/issues/389#issuecomment-2385416577
TRIVY_DB_REPOSITORY: 'public.ecr.aws/aquasecurity/trivy-db:2'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@1fd8a71a1271a5ad7639a423fdd9485e1be64031
uses: github/codeql-action/upload-sarif@17783bfb99b07f70fae080b654aed0c514057477
with:
sarif_file: 'trivy-results.sarif'

View File

@@ -36,7 +36,7 @@ jobs:
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Install Cosign
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
- name: Publish Capsule
id: publish-capsule
uses: peak-scale/github-actions/make-ko-publish@a441cca016861c546ab7e065277e40ce41a3eb84 # v0.2.0

View File

@@ -32,7 +32,7 @@ jobs:
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: 'go.mod'
- uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4
- uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
with:
version: v3.14.2
- name: e2e

View File

@@ -46,7 +46,7 @@ jobs:
chart-digest: ${{ steps.helm_publish.outputs.digest }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
- uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
- name: "Extract Version"
id: extract_version
run: |

View File

@@ -33,7 +33,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4
- uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
- name: Linting Chart
run: helm lint ./charts/capsule

View File

@@ -30,9 +30,9 @@ jobs:
timeout-minutes: 5
continue-on-error: true
- uses: creekorful/goreportcard-action@1f35ced8cdac2cba28c9a2f2288a16aacfd507f9 # v1.0
- uses: anchore/sbom-action/download-syft@da167eac915b4e86f08b264dbdbc867b61be6f0c
- uses: anchore/sbom-action/download-syft@d8a2c0130026bf585de5c176ab8f7ce62d75bf04
- name: Install Cosign
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
with:

View File

@@ -24,7 +24,7 @@ jobs:
with:
persist-credentials: false
- name: Run analysis
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
@@ -37,6 +37,6 @@ jobs:
path: results.sarif
retention-days: 5
- name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with:
sarif_file: results.sarif

View File

@@ -15,7 +15,7 @@ jobs:
pull-requests: write
steps:
- name: Close stale pull requests
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f
uses: actions/stale@65d1d4804d3060875fff9f9fa8a49e27f71ce7f0
with:
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 30 days.'
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 30 days.'

View File

@@ -5,6 +5,7 @@ run:
linters:
default: all
disable:
- godoclint
- depguard
- err113
- exhaustruct

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
rev: v9.22.0
rev: v9.23.0
hooks:
- id: commitlint
stages: [commit-msg]

View File

@@ -7,40 +7,43 @@ This is a list of companies that have adopted Capsule, feel free to open a Pull-
## Adopters list (alphabetically)
### [Bedag Informatik AG](https://www.bedag.ch/)
![Bedag](https://www.bedag.ch/wGlobal/wGlobal/layout/images/logo.svg)
<img src="https://www.bedag.ch/wGlobal/wGlobal/layout/images/logo.svg" alt="Bedag" width="350" />
### [Department of Defense](https://www.defense.gov/)
![United States Department of Defense](https://www.access-board.gov/images/dod-seal.png)
### [KubeRocketCI](https://docs.kuberocketci.io/)
![KubeRocketCI](https://raw.githubusercontent.com/epam/edp-install/master/docs/assets/krci-logo-267×150-white.png)
### [Fastweb](https://www.fastweb.it/)
![Fastweb](https://www.fastweb.it/grandi-aziende/gfx/common/logo-fastweb-header.svg)
### [Klarrio](https://klarrio.com/)
![Klarrio](https://klarrio.com/wp-content/uploads/klarrio.png)
### [PITS Global Data Recovery Services](https://www.pitsdatarecovery.net)
![PITS Global Data Recovery Services](https://www.pitsdatarecovery.net/wp-content/uploads/2020/09/pits-logo.svg)
### [Politecnico di Torino](https://www.polito.it/)
![Politecnico di Torino](https://www.polito.it/themes/custom/polito/logo.svg)
### [Reevo](https://www.reevo.it/)
![Reevo Cloud and CyberSecurity](https://www.dropbox.com/s/x3q6r0oqstgvtdr/Logo_ReeVo_270x200px.svg)
### [Seeweb](https://seeweb.it/en)
![Seeweb x Serverless GPU](https://www.seeweb.it/assets/images/logo-seeweb.svg)
### [University of Torino](https://www.unito.it)
![University of Torino](https://www.unito.it/sites/all/themes/bsunito/img/logo_new_2022.svg)
### [Velocity](https://velocity.tech/)
![Velocity](https://raw.githubusercontent.com/yarelm/velocity-logo/main/velocity.png)
### [Wargaming.net](https://www.wargaming.net/)
![Wargaming.net](https://static-cspbe-eu.wargaming.net/images/logo@2x.png)
<img src="https://www.access-board.gov/images/dod-seal.png" alt="United States Department of Defense" width="350" />
### [Enreach](https://www.enreach.com/)
![Enreach](https://campaigns.enreach.com/hubfs/Global/logos/Enreach-logo-vertical-indigo.svg)
<img src="https://campaigns.enreach.com/hubfs/Global/logos/Enreach-logo-vertical-indigo.svg" alt="Enreach" width="350" />
### [Fastweb](https://www.fastweb.it/)
<img src="https://www.fastweb.it/var/storage_feeds/CMS-Company/articoli/0c2/0c252987b90a18017dedf2ed9feda129/640x360.jpg" alt="Fastweb" width="350" />
### [Klarrio](https://klarrio.com/)
<img src="https://klarrio.com/wp-content/uploads/klarrio.png" alt="Klarrio" width="350" />
### [KubeRocketCI](https://docs.kuberocketci.io/)
<img src="https://raw.githubusercontent.com/epam/edp-install/master/docs/assets/krci-logo-267×150-white.png" alt="KubeRocketCI" width="350" />
### [ODC-Noord](https://odc-noord.nl/)
<img src="./assets/customer_logo/odc-noord-logo.png" alt="ODC-Noord" width="350" />
### [PITS Global Data Recovery Services](https://www.pitsdatarecovery.net)
<img src="https://www.pitsdatarecovery.net/wp-content/uploads/2020/09/pits-logo.svg" alt="PITS Global Data Recovery Services" width="350" />
### [Politecnico di Torino](https://www.polito.it/)
<img src="https://www.polito.it/themes/custom/polito/polito_logo_desktop.svg" alt="Politecnico di Torino" width="350" />
### [Reevo](https://www.reevo.it/)
<img src="https://www.reevo.it/hs-fs/hubfs/logo_reevo_azzurro.png" alt="Reevo Cloud and CyberSecurity" width="350" />
### [Seeweb](https://seeweb.it/en)
<img src="https://www.seeweb.it/assets/images/logo-seeweb.svg" alt="Seeweb x Serverless GPU" width="350" />
### [University of Torino](https://www.unito.it)
<img src="https://www.unito.it/sites/all/themes/bsunito/img/logo_new_2022.svg" alt="University of Torino" width="350" />
### [Velocity](https://velocity.tech/)
<img src="https://raw.githubusercontent.com/yarelm/velocity-logo/main/velocity.png" alt="Velocity" width="350" />
### [Wargaming.net](https://www.wargaming.net/)
<img src="https://download.logo.wine/logo/Wargaming_%28company%29/Wargaming_%28company%29-Logo.wine.png" alt="Wargaming.net" width="350" />

View File

@@ -19,7 +19,7 @@ CAPSULE_IMG ?= $(REGISTRY)/$(IMG_BASE)
CLUSTER_NAME ?= capsule
## Kubernetes Version Support
KUBERNETES_SUPPORTED_VERSION ?= "v1.33.0"
KUBERNETES_SUPPORTED_VERSION ?= "v1.34.0"
## Tool Binaries
KUBECTL ?= kubectl
@@ -151,6 +151,7 @@ dev-setup:
--create-namespace \
--set 'crds.install=true' \
--set 'crds.exclusive=true'\
--set 'crds.createConfig=true'\
--set "webhooks.exclusive=true"\
--set "webhooks.service.url=$${WEBHOOK_URL}" \
--set "webhooks.service.caBundle=$${CA_BUNDLE}" \
@@ -259,7 +260,8 @@ e2e-install: ko-build-all
--set 'manager.resources=null'\
--set "manager.image.tag=$(VERSION)" \
--set 'manager.livenessProbe.failureThreshold=10' \
--set 'manager.readinessProbe.failureThreshold=10' \
--set 'webhooks.hooks.nodes.enabled=true' \
--set "webhooks.exclusive=true"\
capsule \
./charts/capsule
@@ -355,7 +357,7 @@ ginkgo:
$(call go-install-tool,$(GINKGO),github.com/onsi/ginkgo/v2/ginkgo)
CT := $(LOCALBIN)/ct
CT_VERSION := v3.13.0
CT_VERSION := v3.14.0
CT_LOOKUP := helm/chart-testing
ct:
@test -s $(CT) && $(CT) version | grep -q $(CT_VERSION) || \
@@ -383,7 +385,7 @@ nwa:
$(call go-install-tool,$(NWA),github.com/$(NWA_LOOKUP)@$(NWA_VERSION))
GOLANGCI_LINT := $(LOCALBIN)/golangci-lint
GOLANGCI_LINT_VERSION := v2.4.0
GOLANGCI_LINT_VERSION := v2.5.0
GOLANGCI_LINT_LOOKUP := golangci/golangci-lint
golangci-lint: ## Download golangci-lint locally if necessary.
@test -s $(GOLANGCI_LINT) && $(GOLANGCI_LINT) -h | grep -q $(GOLANGCI_LINT_VERSION) || \

View File

@@ -19,7 +19,7 @@ func (in OwnerListSpec) FindOwner(name string, kind OwnerKind) (owner OwnerSpec)
return in[i]
}
return
return owner
}
type ByKindAndName OwnerListSpec

View File

@@ -78,5 +78,5 @@ func (in *Tenant) GetNamespaces() (res []string) {
res = append(res, in.Status.Namespaces...)
return
return res
}

View File

@@ -19,6 +19,11 @@ type CapsuleConfigurationSpec struct {
// Define groups which when found in the request of a user will be ignored by the Capsule
// this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups.
IgnoreUserWithGroups []string `json:"ignoreUserWithGroups,omitempty"`
// ServiceAccounts within tenant namespaces can be promoted to owners of the given tenant
// this can be achieved by labeling the serviceaccount and then they are considered owners. This can only be done by other owners of the tenant.
// However ServiceAccounts which have been promoted to owner can not promote further serviceAccounts.
// +kubebuilder:default=false
AllowServiceAccountPromotion bool `json:"allowServiceAccountPromotion,omitempty"`
// Enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix,
// separated by a dash. This is useful to avoid Namespace name collision in a public CaaS environment.
// +kubebuilder:default=false

View File

@@ -12,6 +12,7 @@ type NamespaceOptions struct {
// Specifies the maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
Quota *int32 `json:"quota,omitempty"`
// Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
// Deprecated: Use additionalMetadataList instead
AdditionalMetadata *api.AdditionalMetadataSpec `json:"additionalMetadata,omitempty"`
// Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant via a list. Optional.
AdditionalMetadataList []api.AdditionalMetadataSelectorSpec `json:"additionalMetadataList,omitempty"`
@@ -19,4 +20,7 @@ type NamespaceOptions struct {
ForbiddenLabels api.ForbiddenListSpec `json:"forbiddenLabels,omitempty"`
// Define the annotations that a Tenant Owner cannot set for their Namespace resources.
ForbiddenAnnotations api.ForbiddenListSpec `json:"forbiddenAnnotations,omitempty"`
// If enabled only metadata from additionalMetadata is reconciled to the namespaces.
//+kubebuilder:default:=false
ManagedMetadataOnly bool `json:"managedMetadataOnly,omitempty"`
}

View File

@@ -13,6 +13,10 @@ type OwnerSpec struct {
ClusterRoles []string `json:"clusterRoles,omitempty"`
// Proxy settings for tenant owner.
ProxyOperations []ProxySettings `json:"proxySettings,omitempty"`
// Additional Labels for the synchronized rolebindings
Labels map[string]string `json:"labels,omitempty"`
// Additional Annotations for the synchronized rolebindings
Annotations map[string]string `json:"annotations,omitempty"`
}
// +kubebuilder:validation:Enum=User;Group;ServiceAccount

View File

@@ -19,7 +19,7 @@ func (o OwnerListSpec) FindOwner(name string, kind OwnerKind) (owner OwnerSpec)
return o[i]
}
return
return owner
}
type ByKindAndName OwnerListSpec

View File

@@ -247,7 +247,7 @@ func (r *ResourcePool) GetNamespaceClaims(namespace string) (claims map[string]*
}
}
return
return claims, claimedResources
}
// Calculate usage for each namespace.
@@ -272,5 +272,5 @@ func (r *ResourcePool) GetClaimedByNamespaceClaims() (claims map[string]corev1.R
}
}
return
return claims
}

View File

@@ -93,7 +93,7 @@ func (in *Tenant) GetSubjectsByClusterRoles(ignoreOwnerKind []OwnerKind) (rolePe
}
}
return
return rolePerms
}
// Get the permissions for a tenant ordered by groups and users.

View File

@@ -28,5 +28,5 @@ func GetTypeLabel(t metav1.Object) (label string, err error) {
err = fmt.Errorf("type %T is not mapped as Capsule label recognized", v)
}
return
return label, err
}

View File

@@ -3,6 +3,12 @@
package v1beta2
import (
k8stypes "k8s.io/apimachinery/pkg/types"
"github.com/projectcapsule/capsule/pkg/meta"
)
// +kubebuilder:validation:Enum=Cordoned;Active
type tenantState string
@@ -18,6 +24,68 @@ type TenantStatus struct {
State tenantState `json:"state"`
// How many namespaces are assigned to the Tenant.
Size uint `json:"size"`
// List of namespaces assigned to the Tenant.
// List of namespaces assigned to the Tenant. (Deprecated)
Namespaces []string `json:"namespaces,omitempty"`
// Tracks state for the namespaces associated with this tenant
Spaces []*TenantStatusNamespaceItem `json:"spaces,omitempty"`
// Tenant Condition
Conditions meta.ConditionList `json:"conditions"`
}
type TenantStatusNamespaceItem struct {
// Conditions
Conditions meta.ConditionList `json:"conditions"`
// Namespace Name
Name string `json:"name"`
// Namespace UID
UID k8stypes.UID `json:"uid,omitempty"`
// Managed Metadata
Metadata *TenantStatusNamespaceMetadata `json:"metadata,omitempty"`
}
type TenantStatusNamespaceMetadata struct {
// Managed Labels
Labels map[string]string `json:"labels,omitempty"`
// Managed Annotations
Annotations map[string]string `json:"annotations,omitempty"`
}
func (ms *TenantStatus) GetInstance(stat *TenantStatusNamespaceItem) *TenantStatusNamespaceItem {
for _, source := range ms.Spaces {
if ms.instancequal(source, stat) {
return source
}
}
return nil
}
func (ms *TenantStatus) UpdateInstance(stat *TenantStatusNamespaceItem) {
// Check if the tenant is already present in the status
for i, source := range ms.Spaces {
if ms.instancequal(source, stat) {
ms.Spaces[i] = stat
return
}
}
ms.Spaces = append(ms.Spaces, stat)
}
func (ms *TenantStatus) RemoveInstance(stat *TenantStatusNamespaceItem) {
// Filter out the datasource with given UID
filter := []*TenantStatusNamespaceItem{}
for _, source := range ms.Spaces {
if !ms.instancequal(source, stat) {
filter = append(filter, source)
}
}
ms.Spaces = filter
}
func (ms *TenantStatus) instancequal(a, b *TenantStatusNamespaceItem) bool {
return a.Name == b.Name
}

View File

@@ -11,8 +11,9 @@ import (
// TenantSpec defines the desired state of Tenant.
type TenantSpec struct {
// Specifies the owners of the Tenant. Mandatory.
Owners OwnerListSpec `json:"owners"`
// Specifies the owners of the Tenant.
// Optional
Owners OwnerListSpec `json:"owners,omitempty"`
// Specifies options for the Namespaces, such as additional metadata or maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
NamespaceOptions *NamespaceOptions `json:"namespaceOptions,omitempty"`
// Specifies options for the Service, such as additional metadata or block of certain type of Services. Optional.
@@ -31,8 +32,10 @@ type TenantSpec struct {
// Specifies the label to control the placement of pods on a given pool of worker nodes. All namespaces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitempty"`
// Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
LimitRanges api.LimitRangesSpec `json:"limitRanges,omitempty"`
// Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional.
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
@@ -73,12 +76,13 @@ type TenantSpec struct {
// +kubebuilder:storageversion
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster,shortName=tnt
// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.state",description="The actual state of the Tenant"
// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.conditions[?(@.type==\"Cordoned\")].reason",description="The actual state of the Tenant"
// +kubebuilder:printcolumn:name="Namespace quota",type="integer",JSONPath=".spec.namespaceOptions.quota",description="The max amount of Namespaces can be created"
// +kubebuilder:printcolumn:name="Namespace count",type="integer",JSONPath=".status.size",description="The total amount of Namespaces in use"
// +kubebuilder:printcolumn:name="Node selector",type="string",JSONPath=".spec.nodeSelector",description="Node Selector applied to Pods"
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Reconcile Status for the tenant"
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="Reconcile Message for the tenant"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
// Tenant is the Schema for the tenants API.
type Tenant struct {
metav1.TypeMeta `json:",inline"`
@@ -93,7 +97,7 @@ func (in *Tenant) GetNamespaces() (res []string) {
res = append(res, in.Status.Namespaces...)
return
return res
}
// +kubebuilder:object:root=true

View File

@@ -9,6 +9,7 @@ package v1beta2
import (
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/meta"
corev1 "k8s.io/api/core/v1"
"k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -461,6 +462,20 @@ func (in *OwnerSpec) DeepCopyInto(out *OwnerSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OwnerSpec.
@@ -1215,6 +1230,24 @@ func (in *TenantStatus) DeepCopyInto(out *TenantStatus) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Spaces != nil {
in, out := &in.Spaces, &out.Spaces
*out = make([]*TenantStatusNamespaceItem, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(TenantStatusNamespaceItem)
(*in).DeepCopyInto(*out)
}
}
}
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make(meta.ConditionList, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantStatus.
@@ -1226,3 +1259,59 @@ func (in *TenantStatus) DeepCopy() *TenantStatus {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantStatusNamespaceItem) DeepCopyInto(out *TenantStatusNamespaceItem) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make(meta.ConditionList, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Metadata != nil {
in, out := &in.Metadata, &out.Metadata
*out = new(TenantStatusNamespaceMetadata)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantStatusNamespaceItem.
func (in *TenantStatusNamespaceItem) DeepCopy() *TenantStatusNamespaceItem {
if in == nil {
return nil
}
out := new(TenantStatusNamespaceItem)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantStatusNamespaceMetadata) DeepCopyInto(out *TenantStatusNamespaceMetadata) {
*out = *in
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantStatusNamespaceMetadata.
func (in *TenantStatusNamespaceMetadata) DeepCopy() *TenantStatusNamespaceMetadata {
if in == nil {
return nil
}
out := new(TenantStatusNamespaceMetadata)
in.DeepCopyInto(out)
return out
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -26,6 +26,7 @@ The following Values have changed key or Value:
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| crds.annnotations | object | `{}` | Extra Annotations for CRDs |
| crds.createConfig | bool | `false` | Create additionally CapsuleConfiguration even if CRDs are exclusive |
| crds.exclusive | bool | `false` | Only install the CRDs, no other primitives |
| crds.install | bool | `true` | Install the CustomResourceDefinitions (This also manages the lifecycle of the CRDs for update operations) |
| crds.labels | object | `{}` | Extra Labels for CRDs |
@@ -105,11 +106,14 @@ The following Values have changed key or Value:
| manager.image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. |
| manager.kind | string | `"Deployment"` | Set the controller deployment mode as `Deployment` or `DaemonSet`. |
| manager.livenessProbe | object | `{"httpGet":{"path":"/healthz","port":10080}}` | Configure the liveness probe using Deployment probe spec |
| manager.options.allowServiceAccountPromotion | bool | `false` | ServiceAccounts within tenant namespaces can be promoted to owners of the given tenant this can be achieved by labeling the serviceaccount and then they are considered owners. This can only be done by other owners of the tenant. However ServiceAccounts which have been promoted to owner can not promote further serviceAccounts. |
| manager.options.annotations | object | `{}` | Additional annotations to add to the CapsuleConfiguration resource |
| manager.options.capsuleConfiguration | string | `"default"` | Change the default name of the capsule configuration name |
| manager.options.capsuleUserGroups | list | `["projectcapsule.dev"]` | Names of the groups considered as Capsule users. |
| manager.options.forceTenantPrefix | bool | `false` | Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash |
| manager.options.generateCertificates | bool | `true` | Specifies whether capsule webhooks certificates should be generated by capsule operator |
| manager.options.ignoreUserWithGroups | list | `[]` | Define groups which when found in the request of a user will be ignored by the Capsule this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups. |
| manager.options.labels | object | `{}` | Additional labels to add to the CapsuleConfiguration resource |
| manager.options.logLevel | string | `"4"` | Set the log verbosity of the capsule with a value from 1 to 10 |
| manager.options.nodeMetadata | object | `{"forbiddenAnnotations":{"denied":[],"deniedRegex":""},"forbiddenLabels":{"denied":[],"deniedRegex":""}}` | Allows to set the forbidden metadata for the worker nodes that could be patched by a Tenant |
| manager.options.protectedNamespaceRegex | string | `""` | If specified, disallows creation of namespaces matching the passed regexp |
@@ -133,7 +137,7 @@ The following Values have changed key or Value:
| monitoring.dashboards.labels | object | `{}` | Labels for dashboard configmaps |
| monitoring.dashboards.namespace | string | `""` | Custom namespace for dashboard configmaps |
| monitoring.dashboards.operator.allowCrossNamespaceImport | bool | `true` | Allow the Operator to match this resource with Grafanas outside the current namespace |
| monitoring.dashboards.operator.enabled | bool | `true` | Enable Operator Resources (GrafanaDashboard) |
| monitoring.dashboards.operator.enabled | bool | `false` | Enable Operator Resources (GrafanaDashboard) |
| monitoring.dashboards.operator.folder | string | `""` | folder assignment for dashboard |
| monitoring.dashboards.operator.instanceSelector | object | `{}` | Selects Grafana instances for import |
| monitoring.dashboards.operator.resyncPeriod | string | `"10m"` | How often the resource is synced, defaults to 10m0s if not set |
@@ -196,7 +200,7 @@ The following Values have changed key or Value:
| webhooks.hooks.networkpolicies.matchPolicy | string | `"Equivalent"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.networkpolicies.namespaceSelector | object | `{"matchExpressions":[{"key":"capsule.clastix.io/tenant","operator":"Exists"}]}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
| webhooks.hooks.networkpolicies.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
| webhooks.hooks.nodes.enabled | bool | `true` | Enable the Hook |
| webhooks.hooks.nodes.enabled | bool | `false` | Enable the Hook |
| webhooks.hooks.nodes.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
| webhooks.hooks.nodes.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.nodes.matchPolicy | string | `"Exact"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
@@ -228,6 +232,12 @@ The following Values have changed key or Value:
| webhooks.hooks.resourcepools.pools.matchPolicy | string | `"Equivalent"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.resourcepools.pools.namespaceSelector | object | `{}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
| webhooks.hooks.resourcepools.pools.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
| webhooks.hooks.serviceaccounts.enabled | bool | `true` | Enable the Hook |
| webhooks.hooks.serviceaccounts.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
| webhooks.hooks.serviceaccounts.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.serviceaccounts.matchPolicy | string | `"Exact"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.serviceaccounts.namespaceSelector | object | `{"matchExpressions":[{"key":"capsule.clastix.io/tenant","operator":"Exists"}]}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
| webhooks.hooks.serviceaccounts.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
| webhooks.hooks.services.enabled | bool | `true` | Enable the Hook |
| webhooks.hooks.services.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
| webhooks.hooks.services.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
@@ -246,6 +256,7 @@ The following Values have changed key or Value:
| webhooks.hooks.tenants.matchPolicy | string | `"Exact"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.tenants.namespaceSelector | object | `{}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
| webhooks.hooks.tenants.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
| webhooks.hooks.tenants.reinvocationPolicy | string | `"Never"` | [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) |
| webhooks.mutatingWebhooksTimeoutSeconds | int | `30` | Timeout in seconds for mutating webhooks |
| webhooks.service.caBundle | string | `""` | CABundle for the webhook service |
| webhooks.service.name | string | `""` | Custom service name for the webhook service |

View File

@@ -40,6 +40,13 @@ spec:
spec:
description: CapsuleConfigurationSpec defines the Capsule configuration.
properties:
allowServiceAccountPromotion:
default: false
description: |-
ServiceAccounts within tenant namespaces can be promoted to owners of the given tenant
this can be achieved by labeling the serviceaccount and then they are considered owners. This can only be done by other owners of the tenant.
However ServiceAccounts which have been promoted to owner can not promote further serviceAccounts.
type: boolean
enableTLSReconciler:
default: true
description: |-

View File

@@ -68,8 +68,18 @@ spec:
the RoleBinding for the given ClusterRole. Optional.
items:
properties:
annotations:
additionalProperties:
type: string
description: Additional Annotations for the synchronized rolebindings
type: object
clusterRoleName:
type: string
labels:
additionalProperties:
type: string
description: Additional Labels for the synchronized rolebindings
type: object
subjects:
description: kubebuilder:validation:Minimum=1
items:
@@ -1041,7 +1051,7 @@ spec:
status: {}
- additionalPrinterColumns:
- description: The actual state of the Tenant
jsonPath: .status.state
jsonPath: .status.conditions[?(@.type=="Cordoned")].reason
name: State
type: string
- description: The max amount of Namespaces can be created
@@ -1056,6 +1066,14 @@ spec:
jsonPath: .spec.nodeSelector
name: Node selector
type: string
- description: Reconcile Status for the tenant
jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- description: Reconcile Message for the tenant
jsonPath: .status.conditions[?(@.type=="Ready")].message
name: Status
type: string
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
@@ -1091,8 +1109,18 @@ spec:
the RoleBinding for the given ClusterRole. Optional.
items:
properties:
annotations:
additionalProperties:
type: string
description: Additional Annotations for the synchronized rolebindings
type: object
clusterRoleName:
type: string
labels:
additionalProperties:
type: string
description: Additional Labels for the synchronized rolebindings
type: object
subjects:
description: kubebuilder:validation:Minimum=1
items:
@@ -1319,9 +1347,9 @@ spec:
type: string
type: object
limitRanges:
description: Specifies the resource min/max usage restrictions to
the Tenant. The assigned values are inherited by any namespace created
in the Tenant. Optional.
description: |-
Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
properties:
items:
items:
@@ -1410,8 +1438,9 @@ spec:
the Tenant owner cannot create further namespaces. Optional.
properties:
additionalMetadata:
description: Specifies additional labels and annotations the Capsule
operator places on any Namespace resource in the Tenant. Optional.
description: |-
Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
Deprecated: Use additionalMetadataList instead
properties:
annotations:
additionalProperties:
@@ -1509,6 +1538,11 @@ spec:
deniedRegex:
type: string
type: object
managedMetadataOnly:
default: false
description: If enabled only metadata from additionalMetadata
is reconciled to the namespaces.
type: boolean
quota:
description: Specifies the maximum number of namespaces allowed
for that Tenant. Once the namespace quota assigned to the Tenant
@@ -1519,9 +1553,9 @@ spec:
type: integer
type: object
networkPolicies:
description: Specifies the NetworkPolicies assigned to the Tenant.
The assigned NetworkPolicies are inherited by any namespace created
in the Tenant. Optional.
description: |-
Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
properties:
items:
items:
@@ -2007,9 +2041,16 @@ spec:
label. Optional.
type: object
owners:
description: Specifies the owners of the Tenant. Mandatory.
description: |-
Specifies the owners of the Tenant.
Optional
items:
properties:
annotations:
additionalProperties:
type: string
description: Additional Annotations for the synchronized rolebindings
type: object
clusterRoles:
default:
- admin
@@ -2027,6 +2068,11 @@ spec:
- Group
- ServiceAccount
type: string
labels:
additionalProperties:
type: string
description: Additional Labels for the synchronized rolebindings
type: object
name:
description: Name of tenant owner.
type: string
@@ -2417,20 +2463,163 @@ spec:
type: object
type: object
x-kubernetes-map-type: atomic
required:
- owners
type: object
status:
description: Returns the observed state of the Tenant.
properties:
conditions:
description: Tenant Condition
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
namespaces:
description: List of namespaces assigned to the Tenant.
description: List of namespaces assigned to the Tenant. (Deprecated)
items:
type: string
type: array
size:
description: How many namespaces are assigned to the Tenant.
type: integer
spaces:
description: Tracks state for the namespaces associated with this
tenant
items:
properties:
conditions:
description: Conditions
items:
description: Condition contains details for one aspect of
the current state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False,
Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
metadata:
description: Managed Metadata
properties:
annotations:
additionalProperties:
type: string
description: Managed Annotations
type: object
labels:
additionalProperties:
type: string
description: Managed Labels
type: object
type: object
name:
description: Namespace Name
type: string
uid:
description: Namespace UID
type: string
required:
- conditions
- name
type: object
type: array
state:
default: Active
description: The operational state of the Tenant. Possible values
@@ -2440,6 +2629,7 @@ spec:
- Active
type: string
required:
- conditions
- size
- state
type: object

View File

@@ -18,7 +18,9 @@ spec:
{{- if .Values.podSecurityContext.enabled }}
securityContext: {{- omit .Values.podSecurityContext "enabled" | toYaml | nindent 4 }}
{{- end }}
{{- if not .Values.manager.hostUsers }}
hostUsers: {{ .Values.manager.hostUsers }}
{{- end }}
{{- if .Values.manager.hostNetwork }}
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet

View File

@@ -1,12 +1,15 @@
{{- if not $.Values.crds.exclusive }}
{{- if or (not $.Values.crds.exclusive) ($.Values.crds.createConfig) }}
apiVersion: capsule.clastix.io/v1beta2
kind: CapsuleConfiguration
metadata:
name: default
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.manager.options.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
annotations:
{{- with .Values.customAnnotations }}
{{- with (mergeOverwrite .Values.customAnnotations .Values.manager.options.annotations) }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
@@ -16,6 +19,7 @@ spec:
TLSSecretName: {{ include "capsule.secretTlsName" . }}
validatingWebhookConfigurationName: {{ include "capsule.fullname" . }}-validating-webhook-configuration
forceTenantPrefix: {{ .Values.manager.options.forceTenantPrefix }}
allowServiceAccountPromotion: {{ .Values.manager.options.allowServiceAccountPromotion }}
userGroups:
{{- toYaml .Values.manager.options.capsuleUserGroups | nindent 4 }}
userNames:

View File

@@ -23,11 +23,19 @@ rules:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
resourceNames:
- capsuleconfigurations.capsule.clastix.io
- resourcepoolclaims.capsule.clastix.io
- resourcepools.capsule.clastix.io
- tenantresources.capsule.clastix.io
- globaltenantresources.capsule.clastix.io
- tenants.capsule.clastix.io
verbs:
- create
- delete
- get
- patch
- update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding

View File

@@ -274,4 +274,44 @@ webhooks:
timeoutSeconds: {{ $.Values.webhooks.mutatingWebhooksTimeoutSeconds }}
{{- end }}
{{- end }}
{{- with .Values.webhooks.hooks.tenants }}
{{- if .enabled }}
- name: tenants.projectcapsule.dev
admissionReviewVersions:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/tenants/mutating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
reinvocationPolicy: {{ .reinvocationPolicy }}
{{- with .namespaceSelector }}
namespaceSelector:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .objectSelector }}
objectSelector:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .matchConditions }}
matchConditions:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
- apiGroups:
- capsule.clastix.io
apiVersions:
- v1beta2
operations:
- CREATE
- UPDATE
- DELETE
resources:
- tenants
scope: 'Cluster'
sideEffects: None
timeoutSeconds: {{ $.Values.webhooks.mutatingWebhooksTimeoutSeconds }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -261,6 +261,7 @@ webhooks:
- UPDATE
resources:
- pods
- pods/ephemeralcontainers
scope: Namespaced
sideEffects: None
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
@@ -382,7 +383,7 @@ webhooks:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/tenants" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/tenants/validating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}
@@ -408,7 +409,7 @@ webhooks:
- DELETE
resources:
- tenants
scope: '*'
scope: 'Cluster'
sideEffects: None
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
{{- end }}
@@ -480,6 +481,7 @@ webhooks:
operations:
- CREATE
- UPDATE
- DELETE
resources:
- resourcepoolclaims
scope: '*'
@@ -525,4 +527,41 @@ webhooks:
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
{{- end }}
{{- end }}
{{- with .Values.webhooks.hooks.serviceaccounts }}
{{- if .enabled }}
- name: serviceaccounts.tenant.projectcapsule.dev
admissionReviewVersions:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/serviceaccounts" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}
namespaceSelector:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .objectSelector }}
objectSelector:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .matchConditions }}
matchConditions:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
- apiGroups:
- '*'
apiVersions:
- '*'
operations:
- CREATE
- UPDATE
resources:
- 'serviceaccounts'
scope: Namespaced
sideEffects: None
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -26,6 +26,10 @@
"description": "Extra Annotations for CRDs",
"type": "object"
},
"createConfig": {
"description": "Create additionally CapsuleConfiguration even if CRDs are exclusive",
"type": "boolean"
},
"exclusive": {
"description": "Only install the CRDs, no other primitives",
"type": "boolean"
@@ -289,6 +293,14 @@
"options": {
"type": "object",
"properties": {
"allowServiceAccountPromotion": {
"description": "ServiceAccounts within tenant namespaces can be promoted to owners of the given tenant this can be achieved by labeling the serviceaccount and then they are considered owners. This can only be done by other owners of the tenant. However ServiceAccounts which have been promoted to owner can not promote further serviceAccounts.",
"type": "boolean"
},
"annotations": {
"description": "Additional annotations to add to the CapsuleConfiguration resource",
"type": "object"
},
"capsuleConfiguration": {
"description": "Change the default name of the capsule configuration name",
"type": "string"
@@ -312,6 +324,10 @@
"description": "Define groups which when found in the request of a user will be ignored by the Capsule this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups.",
"type": "array"
},
"labels": {
"description": "Additional labels to add to the CapsuleConfiguration resource",
"type": "object"
},
"logLevel": {
"description": "Set the log verbosity of the capsule with a value from 1 to 10",
"type": "string"
@@ -1194,6 +1210,51 @@
}
}
},
"serviceaccounts": {
"type": "object",
"properties": {
"enabled": {
"description": "Enable the Hook",
"type": "boolean"
},
"failurePolicy": {
"description": "[FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy)",
"type": "string"
},
"matchConditions": {
"description": "[MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)",
"type": "array"
},
"matchPolicy": {
"description": "[MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)",
"type": "string"
},
"namespaceSelector": {
"description": "[NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector)",
"type": "object",
"properties": {
"matchExpressions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"operator": {
"type": "string"
}
}
}
}
}
},
"objectSelector": {
"description": "[ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector)",
"type": "object"
}
}
},
"services": {
"type": "object",
"properties": {
@@ -1326,6 +1387,10 @@
"objectSelector": {
"description": "[ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector)",
"type": "object"
},
"reinvocationPolicy": {
"description": "[ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)",
"type": "string"
}
}
}

View File

@@ -65,6 +65,8 @@ crds:
install: true
# -- Only install the CRDs, no other primitives
exclusive: false
# -- Create additionally CapsuleConfiguration even if CRDs are exclusive
createConfig: false
# -- Extra Labels for CRDs
labels: {}
# -- Extra Annotations for CRDs
@@ -156,12 +158,14 @@ manager:
# Additional Capsule Controller Options
options:
# -- Additional labels to add to the CapsuleConfiguration resource
labels: {}
# -- Additional annotations to add to the CapsuleConfiguration resource
annotations: {}
# -- Change the default name of the capsule configuration name
capsuleConfiguration: default
# -- Set the log verbosity of the capsule with a value from 1 to 10
logLevel: '4'
# -- Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash
forceTenantPrefix: false
# -- Names of the users considered as Capsule users.
userNames: []
# -- Names of the groups considered as Capsule users.
@@ -169,6 +173,12 @@ manager:
# -- Define groups which when found in the request of a user will be ignored by the Capsule
# this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups.
ignoreUserWithGroups: []
# -- ServiceAccounts within tenant namespaces can be promoted to owners of the given tenant
# this can be achieved by labeling the serviceaccount and then they are considered owners. This can only be done by other owners of the tenant.
# However ServiceAccounts which have been promoted to owner can not promote further serviceAccounts.
allowServiceAccountPromotion: false
# -- Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash
forceTenantPrefix: false
# -- If specified, disallows creation of namespaces matching the passed regexp
protectedNamespaceRegex: ""
# -- Specifies whether capsule webhooks certificates should be generated by capsule operator
@@ -218,9 +228,6 @@ imagePullSecrets: []
# -- Labels to add to the capsule pod.
podLabels: {}
# The following annotations guarantee scheduling for critical add-on pods
# podAnnotations:
# scheduler.alpha.kubernetes.io/critical-pod: ''
# -- Annotations to add to the capsule pod.
podAnnotations: {}
@@ -311,7 +318,7 @@ monitoring:
# Grafana Operator
operator:
# -- Enable Operator Resources (GrafanaDashboard)
enabled: true
enabled: false
# -- Allow the Operator to match this resource with Grafanas outside the current namespace
allowCrossNamespaceImport: true
# -- How often the resource is synced, defaults to 10m0s if not set
@@ -569,6 +576,8 @@ webhooks:
namespaceSelector: {}
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
matchConditions: []
# -- [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)
reinvocationPolicy: Never
tenantResourceObjects:
# -- Enable the Hook
@@ -609,7 +618,7 @@ webhooks:
nodes:
# -- Enable the Hook
enabled: true
enabled: false
# -- [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy)
failurePolicy: Fail
# -- [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
@@ -621,6 +630,23 @@ webhooks:
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
matchConditions: []
serviceaccounts:
# -- Enable the Hook
enabled: true
# -- [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy)
failurePolicy: Fail
# -- [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
matchPolicy: Exact
# -- [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector)
objectSelector: {}
# -- [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector)
namespaceSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
matchConditions: []
# -- Deprecated, use webhooks.hooks.namespaces instead
namespaceOwnerReference: {}

View File

@@ -56,7 +56,9 @@ import (
"github.com/projectcapsule/capsule/pkg/webhook/resourcepool"
"github.com/projectcapsule/capsule/pkg/webhook/route"
"github.com/projectcapsule/capsule/pkg/webhook/service"
"github.com/projectcapsule/capsule/pkg/webhook/tenant"
"github.com/projectcapsule/capsule/pkg/webhook/serviceaccounts"
tenantmutation "github.com/projectcapsule/capsule/pkg/webhook/tenant/mutation"
tenantvalidation "github.com/projectcapsule/capsule/pkg/webhook/tenant/validation"
tntresource "github.com/projectcapsule/capsule/pkg/webhook/tenantresource"
"github.com/projectcapsule/capsule/pkg/webhook/utils"
)
@@ -226,18 +228,20 @@ func main() {
// webhooks: the order matters, don't change it and just append
webhooksList := append(
make([]webhook.Webhook, 0),
route.Pod(pod.ImagePullPolicy(), pod.ContainerRegistry(), pod.PriorityClass(), pod.RuntimeClass()),
route.Namespace(utils.InCapsuleGroups(cfg, namespacevalidation.PatchHandler(), namespacevalidation.QuotaHandler(), namespacevalidation.FreezeHandler(cfg), namespacevalidation.PrefixHandler(cfg), namespacevalidation.UserMetadataHandler())),
route.Pod(pod.ImagePullPolicy(), pod.ContainerRegistry(cfg), pod.PriorityClass(), pod.RuntimeClass()),
route.Namespace(utils.InCapsuleGroups(cfg, namespacevalidation.PatchHandler(cfg), namespacevalidation.QuotaHandler(), namespacevalidation.FreezeHandler(cfg), namespacevalidation.PrefixHandler(cfg), namespacevalidation.UserMetadataHandler())),
route.Ingress(ingress.Class(cfg, kubeVersion), ingress.Hostnames(cfg), ingress.Collision(cfg), ingress.Wildcard()),
route.PVC(pvc.Validating(), pvc.PersistentVolumeReuse()),
route.Service(service.Handler()),
route.TenantResourceObjects(utils.InCapsuleGroups(cfg, tntresource.WriteOpsHandler())),
route.NetworkPolicy(utils.InCapsuleGroups(cfg, networkpolicy.Handler())),
route.Tenant(tenant.NameHandler(), tenant.RoleBindingRegexHandler(), tenant.IngressClassRegexHandler(), tenant.StorageClassRegexHandler(), tenant.ContainerRegistryRegexHandler(), tenant.HostnameRegexHandler(), tenant.FreezedEmitter(), tenant.ServiceAccountNameHandler(), tenant.ForbiddenAnnotationsRegexHandler(), tenant.ProtectedHandler(), tenant.MetaHandler()),
route.Cordoning(tenant.CordoningHandler(cfg)),
route.TenantMutating(tenantmutation.MetaHandler()),
route.TenantValidating(tenantvalidation.NameHandler(), tenantvalidation.RoleBindingRegexHandler(), tenantvalidation.IngressClassRegexHandler(), tenantvalidation.StorageClassRegexHandler(), tenantvalidation.ContainerRegistryRegexHandler(), tenantvalidation.HostnameRegexHandler(), tenantvalidation.FreezedEmitter(), tenantvalidation.ServiceAccountNameHandler(), tenantvalidation.ForbiddenAnnotationsRegexHandler(), tenantvalidation.ProtectedHandler()),
route.Cordoning(tenantvalidation.CordoningHandler(cfg)),
route.Node(utils.InCapsuleGroups(cfg, node.UserMetadataHandler(cfg, kubeVersion))),
route.ServiceAccounts(serviceaccounts.Handler(cfg)),
route.NamespacePatch(utils.InCapsuleGroups(cfg, namespacemutation.CordoningLabelHandler(cfg), namespacemutation.OwnerReferenceHandler(cfg), namespacemutation.MetadataHandler(cfg))),
route.CustomResources(tenant.ResourceCounterHandler(manager.GetClient())),
route.CustomResources(tenantvalidation.ResourceCounterHandler(manager.GetClient())),
route.Gateway(gateway.Class(cfg)),
route.Defaults(defaults.Handler(cfg, kubeVersion)),
route.ResourcePoolMutation((resourcepool.PoolMutationHandler(ctrl.Log.WithName("webhooks").WithName("resourcepool")))),

View File

@@ -42,5 +42,5 @@ func (c *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
c.Log.Info("CapsuleConfiguration reconciliation finished", "request.name", request.Name)
return
return res, err
}

View File

@@ -9,9 +9,11 @@ import (
"fmt"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/util/retry"
"k8s.io/client-go/util/workqueue"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -23,6 +25,7 @@ import (
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/controllers/utils"
"github.com/projectcapsule/capsule/pkg/configuration"
"github.com/projectcapsule/capsule/pkg/meta"
)
type Manager struct {
@@ -47,17 +50,31 @@ func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, config
Watches(&capsulev1beta2.CapsuleConfiguration{}, handler.Funcs{
UpdateFunc: func(ctx context.Context, updateEvent event.TypedUpdateEvent[client.Object], limitingInterface workqueue.TypedRateLimitingInterface[reconcile.Request]) {
if updateEvent.ObjectNew.GetName() == configurationName {
if crbErr := r.EnsureClusterRoleBindings(ctx); crbErr != nil {
if crbErr := r.EnsureClusterRoleBindingsProvisioner(ctx); crbErr != nil {
r.Log.Error(err, "cannot update ClusterRoleBinding upon CapsuleConfiguration update")
}
}
},
}).Complete(r)
}).
Watches(&corev1.ServiceAccount{}, handler.Funcs{
CreateFunc: func(ctx context.Context, e event.TypedCreateEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
r.handleSAChange(ctx, e.Object)
},
UpdateFunc: func(ctx context.Context, e event.TypedUpdateEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
if promotionLabelsChanged(e.ObjectOld.GetLabels(), e.ObjectNew.GetLabels()) {
r.handleSAChange(ctx, e.ObjectNew)
}
},
DeleteFunc: func(ctx context.Context, e event.TypedDeleteEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
r.handleSAChange(ctx, e.Object)
},
}).
Complete(r)
if crbErr != nil {
err = errors.Join(err, crbErr)
}
return
return err
}
// Reconcile serves both required ClusterRole and ClusterRoleBinding resources: that's ok, we're watching for multiple
@@ -71,8 +88,8 @@ func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
break
}
if err = r.EnsureClusterRoleBindings(ctx); err != nil {
r.Log.Error(err, "Reconciliation for ClusterRoleBindings failed")
if err = r.EnsureClusterRoleBindingsProvisioner(ctx); err != nil {
r.Log.Error(err, "Reconciliation for ClusterRoleBindings (Provisioner) failed")
break
}
@@ -82,39 +99,55 @@ func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
}
}
return
return res, err
}
func (r *Manager) EnsureClusterRoleBindings(ctx context.Context) (err error) {
func (r *Manager) EnsureClusterRoleBindingsProvisioner(ctx context.Context) error {
crb := &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: ProvisionerRoleName,
},
ObjectMeta: metav1.ObjectMeta{Name: ProvisionerRoleName},
}
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, crb, func() (err error) {
crb.RoleRef = provisionerClusterRoleBinding.RoleRef
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, crb, func() error {
crb.RoleRef = provisionerClusterRoleBinding.RoleRef
crb.Subjects = nil
crb.Subjects = []rbacv1.Subject{}
for _, group := range r.Configuration.UserGroups() {
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
Kind: rbacv1.GroupKind,
Name: group,
})
}
for _, group := range r.Configuration.UserGroups() {
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
Kind: "Group",
Name: group,
})
}
for _, user := range r.Configuration.UserNames() {
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
Kind: rbacv1.UserKind,
Name: user,
})
}
for _, user := range r.Configuration.UserNames() {
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
Kind: "User",
Name: user,
})
}
if r.Configuration.AllowServiceAccountPromotion() {
saList := &corev1.ServiceAccountList{}
if err := r.Client.List(ctx, saList, client.MatchingLabels{
meta.OwnerPromotionLabel: meta.OwnerPromotionLabelTrigger,
}); err != nil {
return err
}
return
for _, sa := range saList.Items {
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
Kind: rbacv1.ServiceAccountKind,
Name: sa.Name,
Namespace: sa.Namespace,
})
}
}
return nil
})
return err
})
return
}
func (r *Manager) EnsureClusterRole(ctx context.Context, roleName string) (err error) {
@@ -135,7 +168,7 @@ func (r *Manager) EnsureClusterRole(ctx context.Context, roleName string) (err e
return nil
})
return
return err
}
// Start is the Runnable function triggered upon Manager start-up to perform the first RBAC reconciliation
@@ -156,7 +189,7 @@ func (r *Manager) Start(ctx context.Context) error {
r.Log.Info("setting up ClusterRoleBindings")
if err := r.EnsureClusterRoleBindings(ctx); err != nil {
if err := r.EnsureClusterRoleBindingsProvisioner(ctx); err != nil {
if apierrors.IsAlreadyExists(err) {
return nil
}
@@ -166,3 +199,30 @@ func (r *Manager) Start(ctx context.Context) error {
return nil
}
func (r *Manager) handleSAChange(ctx context.Context, obj client.Object) {
if !r.Configuration.AllowServiceAccountPromotion() {
return
}
if err := r.EnsureClusterRoleBindingsProvisioner(ctx); err != nil {
r.Log.Error(err, "cannot update ClusterRoleBinding upon ServiceAccount event")
}
}
func promotionLabelsChanged(oldLabels, newLabels map[string]string) bool {
keys := []string{
meta.OwnerPromotionLabel,
}
for _, key := range keys {
oldVal, oldOK := oldLabels[key]
newVal, newOK := newLabels[key]
if oldOK != newOK || oldVal != newVal {
return true
}
}
return false
}

View File

@@ -61,7 +61,7 @@ func (r resourceClaimController) Reconcile(ctx context.Context, request ctrl.Req
log.Error(err, "Error reading the object")
return
return result, err
}
// Ensuring the Quota Status
@@ -291,5 +291,5 @@ func updateStatusAndEmitEvent(
claim.Status.Condition.Message,
)
return
return err
}

View File

@@ -85,7 +85,7 @@ func (r resourcePoolController) Reconcile(ctx context.Context, request ctrl.Requ
log.Error(err, "Error reading the object")
return
return result, err
}
// ResourceQuota Reconciliation
@@ -298,7 +298,7 @@ func (r *resourcePoolController) canClaimWithinNamespace(
}
}
return
return res
}
// Handles exhaustions when a exhaustion was already declared in the given map.
@@ -336,7 +336,7 @@ func (r *resourcePoolController) handleClaimOrderedExhaustion(
return queued, updateStatusAndEmitEvent(ctx, r.Client, r.recorder, claim, cond)
}
return
return queued, err
}
func (r *resourcePoolController) handleClaimResourceExhaustion(
@@ -399,12 +399,12 @@ func (r *resourcePoolController) handleClaimToPoolBinding(
cond.Message = "Claimed resources"
if err = updateStatusAndEmitEvent(ctx, r.Client, r.recorder, claim, cond); err != nil {
return
return err
}
pool.AddClaimToStatus(claim)
return
return err
}
// Attempts to garbage collect a ResourceQuota resource.
@@ -571,7 +571,7 @@ func (r *resourcePoolController) gatherMatchingNamespaces(
seenNamespaces := make(map[string]struct{})
if !pool.DeletionTimestamp.IsZero() {
return
return namespaces, err
}
for _, selector := range pool.Spec.Selectors {
@@ -597,7 +597,7 @@ func (r *resourcePoolController) gatherMatchingNamespaces(
}
}
return
return namespaces, err
}
// Get Currently selected claims for the resourcepool.

View File

@@ -5,12 +5,15 @@ package tenant
import (
"context"
"fmt"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
@@ -20,6 +23,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/meta"
"github.com/projectcapsule/capsule/pkg/metrics"
)
@@ -43,7 +47,6 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager) error {
Complete(r)
}
//nolint:nakedret
func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ctrl.Result, err error) {
r.Log = r.Log.WithValues("Request.Name", request.Name)
// Fetch the Tenant instance
@@ -53,98 +56,82 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
r.Log.Info("Request object not found, could have been deleted after reconcile request")
// If tenant was deleted or cannot be found, clean up metrics
r.Metrics.DeleteAllMetrics(request.Name)
r.Metrics.DeleteAllMetricsForTenant(request.Name)
return reconcile.Result{}, nil
}
r.Log.Error(err, "Error reading the object")
return
return result, err
}
preRecNamespaces := instance.Status.Namespaces
defer func() {
r.syncTenantStatusMetrics(instance)
// Ensuring the Tenant Status
if err = r.updateTenantStatus(ctx, instance); err != nil {
r.Log.Error(err, "Cannot update Tenant status")
if uerr := r.updateTenantStatus(ctx, instance, err); uerr != nil {
err = fmt.Errorf("cannot update tenant status: %w", uerr)
return
}
// Ensuring Metadata
return
}
}()
// Ensuring Metadata.
if err = r.ensureMetadata(ctx, instance); err != nil {
r.Log.Error(err, "Cannot ensure metadata")
err = fmt.Errorf("cannot ensure metadata: %w", err)
return
return result, err
}
// Ensuring ResourceQuota
r.Log.Info("Ensuring limit resources count is updated")
if err = r.syncCustomResourceQuotaUsages(ctx, instance); err != nil {
r.Log.Error(err, "Cannot count limited resources")
err = fmt.Errorf("cannot count limited resources: %w", err)
return
return result, err
}
// Ensuring all namespaces are collected
r.Log.Info("Ensuring all Namespaces are collected")
if err = r.collectNamespaces(ctx, instance); err != nil {
r.Log.Error(err, "Cannot collect Namespace resources")
return
}
// Ensuring Status metrics are exposed
r.Log.Info("Ensuring all status metrics are exposed")
r.syncStatusMetrics(instance, preRecNamespaces)
// Ensuring Namespace metadata
// Reconcile Namespaces
r.Log.Info("Starting processing of Namespaces", "items", len(instance.Status.Namespaces))
if err = r.syncNamespaces(ctx, instance); err != nil {
r.Log.Error(err, "Cannot sync Namespace items")
if err = r.reconcileNamespaces(ctx, instance); err != nil {
err = fmt.Errorf("namespace(s) had reconciliation errors")
return
return result, err
}
// Ensuring NetworkPolicy resources
r.Log.Info("Starting processing of Network Policies")
if err = r.syncNetworkPolicies(ctx, instance); err != nil {
r.Log.Error(err, "Cannot sync NetworkPolicy items")
err = fmt.Errorf("cannot sync networkPolicy items: %w", err)
return
return result, err
}
// Ensuring LimitRange resources
r.Log.Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges.Items))
if err = r.syncLimitRanges(ctx, instance); err != nil {
r.Log.Error(err, "Cannot sync LimitRange items")
err = fmt.Errorf("cannot sync limitrange items: %w", err)
return
return result, err
}
// Ensuring ResourceQuota resources
r.Log.Info("Starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota.Items))
if err = r.syncResourceQuotas(ctx, instance); err != nil {
r.Log.Error(err, "Cannot sync ResourceQuota items")
err = fmt.Errorf("cannot sync resourcequota items: %w", err)
return
return result, err
}
// Ensuring RoleBinding resources
r.Log.Info("Ensuring RoleBindings for Owners and Tenant")
if err = r.syncRoleBindings(ctx, instance); err != nil {
r.Log.Error(err, "Cannot sync RoleBindings items")
err = fmt.Errorf("cannot sync rolebindings items: %w", err)
return
}
// Ensuring Namespace count
r.Log.Info("Ensuring Namespace count")
if err = r.ensureNamespaceCount(ctx, instance); err != nil {
r.Log.Error(err, "Cannot sync Namespace count")
return
return result, err
}
r.Log.Info("Tenant reconciling completed")
@@ -152,14 +139,40 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
return ctrl.Result{}, err
}
func (r *Manager) updateTenantStatus(ctx context.Context, tnt *capsulev1beta2.Tenant) error {
func (r *Manager) updateTenantStatus(ctx context.Context, tnt *capsulev1beta2.Tenant, reconcileError error) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
if tnt.Spec.Cordoned {
tnt.Status.State = capsulev1beta2.TenantStateCordoned
} else {
tnt.Status.State = capsulev1beta2.TenantStateActive
latest := &capsulev1beta2.Tenant{}
if err = r.Get(ctx, types.NamespacedName{Name: tnt.GetName()}, latest); err != nil {
return err
}
return r.Client.Status().Update(ctx, tnt)
latest.Status = tnt.Status
// Set Ready Condition
readyCondition := meta.NewReadyCondition(tnt)
if reconcileError != nil {
readyCondition.Message = reconcileError.Error()
readyCondition.Status = metav1.ConditionFalse
readyCondition.Reason = meta.FailedReason
}
latest.Status.Conditions.UpdateConditionByType(readyCondition)
// Set Cordoned Condition
cordonedCondition := meta.NewCordonedCondition(tnt)
if tnt.Spec.Cordoned {
latest.Status.State = capsulev1beta2.TenantStateCordoned
cordonedCondition.Reason = meta.CordonedReason
cordonedCondition.Message = "Tenant is cordoned"
cordonedCondition.Status = metav1.ConditionTrue
} else {
latest.Status.State = capsulev1beta2.TenantStateActive
}
latest.Status.Conditions.UpdateConditionByType(cordonedCondition)
return r.Client.Status().Update(ctx, latest)
})
}

View File

@@ -6,6 +6,8 @@ package tenant
import (
"context"
"k8s.io/apimachinery/pkg/types"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
capsuleapi "github.com/projectcapsule/capsule/pkg/api"
)
@@ -17,7 +19,13 @@ func (r *Manager) ensureMetadata(ctx context.Context, tnt *capsulev1beta2.Tenant
tnt.Labels = make(map[string]string)
}
tnt.Labels[capsuleapi.TenantNameLabel] = tnt.Name
if v, ok := tnt.Labels[capsuleapi.TenantNameLabel]; ok && v == tnt.Name {
return err
}
return r.Update(ctx, tnt)
if err := r.Update(ctx, tnt); err != nil {
return err
}
return r.Get(ctx, types.NamespacedName{Name: tnt.GetName()}, tnt)
}

View File

@@ -3,32 +3,58 @@
package tenant
import (
"slices"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/meta"
)
// Exposing Status Metrics for tenant.
func (r *Manager) syncStatusMetrics(tenant *capsulev1beta2.Tenant, preRecNamespaces []string) {
var cordoned float64 = 0
func (r *Manager) syncTenantStatusMetrics(tenant *capsulev1beta2.Tenant) {
// Expose namespace-tenant relationship
for _, ns := range tenant.Status.Namespaces {
r.Metrics.TenantNamespaceRelationshipGauge.WithLabelValues(tenant.GetName(), ns).Set(1)
}
// Cleanup deleted namespaces
for _, ns := range preRecNamespaces {
if !slices.Contains(tenant.Status.Namespaces, ns) {
r.Metrics.DeleteNamespaceRelationshipMetrics(ns)
}
}
if tenant.Spec.Cordoned {
cordoned = 1
}
// Expose cordoned status
r.Metrics.TenantNamespaceCounterGauge.WithLabelValues(tenant.Name).Set(float64(tenant.Status.Size))
// Expose the namespace counter
r.Metrics.TenantCordonedStatusGauge.WithLabelValues(tenant.Name).Set(cordoned)
// Expose Status Metrics
for _, status := range []string{meta.ReadyCondition, meta.CordonedCondition} {
var value float64
cond := tenant.Status.Conditions.GetConditionByType(status)
if cond == nil {
r.Metrics.DeleteTenantConditionMetricByType(tenant.Name, status)
continue
}
if cond.Status == metav1.ConditionTrue {
value = 1
}
r.Metrics.TenantConditionGauge.WithLabelValues(tenant.GetName(), status).Set(value)
}
}
// Exposing Status Metrics for tenant.
func (r *Manager) syncNamespaceStatusMetrics(tenant *capsulev1beta2.Tenant, namespace *corev1.Namespace) {
for _, status := range []string{meta.ReadyCondition, meta.CordonedCondition} {
var value float64
cond := tenant.Status.Conditions.GetConditionByType(status)
if cond == nil {
r.Metrics.DeleteTenantNamespaceConditionMetricByType(namespace.Name, status)
continue
}
if cond.Status == metav1.ConditionTrue {
value = 1
}
r.Metrics.TenantNamespaceConditionGauge.WithLabelValues(tenant.GetName(), namespace.GetName(), status).Set(value)
}
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/valyala/fasttemplate"
"golang.org/x/sync/errgroup"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
@@ -20,51 +21,212 @@ import (
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/meta"
"github.com/projectcapsule/capsule/pkg/utils"
)
// Ensuring all annotations are applied to each Namespace handled by the Tenant.
func (r *Manager) syncNamespaces(ctx context.Context, tenant *capsulev1beta2.Tenant) (err error) {
func (r *Manager) reconcileNamespaces(ctx context.Context, tenant *capsulev1beta2.Tenant) (err error) {
if err = r.collectNamespaces(ctx, tenant); err != nil {
err = fmt.Errorf("cannot collect namespaces: %w", err)
return err
}
gcSet := make(map[string]struct{})
for _, inst := range tenant.Status.Spaces {
gcSet[inst.Name] = struct{}{}
}
group := new(errgroup.Group)
for _, item := range tenant.Status.Namespaces {
namespace := item
delete(gcSet, namespace)
group.Go(func() error {
return r.syncNamespaceMetadata(ctx, namespace, tenant)
return r.reconcileNamespace(ctx, namespace, tenant)
})
}
if err = group.Wait(); err != nil {
r.Log.Error(err, "Cannot sync Namespaces")
err = fmt.Errorf("cannot sync Namespaces: %w", err)
}
return
for name := range gcSet {
r.Metrics.DeleteAllMetricsForNamespace(name)
tenant.Status.RemoveInstance(&capsulev1beta2.TenantStatusNamespaceItem{
Name: name,
})
}
tenant.Status.Size = uint(len(tenant.Status.Namespaces))
return err
}
func (r *Manager) syncNamespaceMetadata(ctx context.Context, namespace string, tnt *capsulev1beta2.Tenant) (err error) {
var res controllerutil.OperationResult
func (r *Manager) reconcileNamespace(ctx context.Context, namespace string, tnt *capsulev1beta2.Tenant) (err error) {
ns := &corev1.Namespace{}
if err = r.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil {
return err
}
err = retry.RetryOnConflict(retry.DefaultBackoff, func() (conflictErr error) {
ns := &corev1.Namespace{}
if conflictErr = r.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil {
return conflictErr
stat := &capsulev1beta2.TenantStatusNamespaceItem{
Name: namespace,
UID: ns.GetUID(),
}
metaStatus := &capsulev1beta2.TenantStatusNamespaceMetadata{}
// Always update tenant status condition after reconciliation
defer func() {
instance := tnt.Status.GetInstance(stat)
if instance != nil {
stat = instance
}
res, conflictErr = controllerutil.CreateOrUpdate(ctx, r.Client, ns, func() error {
return SyncNamespaceMetadata(tnt, ns)
readCondition := meta.NewReadyCondition(ns)
if err != nil {
readCondition.Status = metav1.ConditionFalse
readCondition.Reason = meta.FailedReason
readCondition.Message = fmt.Sprintf("Failed to reconcile: %v", err)
if instance != nil && instance.Metadata != nil {
stat.Metadata = instance.Metadata
}
} else if metaStatus != nil {
stat.Metadata = metaStatus
}
stat.Conditions.UpdateConditionByType(readCondition)
cordonedCondition := meta.NewCordonedCondition(ns)
if ns.Labels[meta.CordonedLabel] == meta.CordonedLabelTrigger {
cordonedCondition.Reason = meta.CordonedReason
cordonedCondition.Message = "namespace is cordoned"
cordonedCondition.Status = metav1.ConditionTrue
}
stat.Conditions.UpdateConditionByType(cordonedCondition)
tnt.Status.UpdateInstance(stat)
r.syncNamespaceStatusMetrics(tnt, ns)
}()
err = retry.RetryOnConflict(retry.DefaultBackoff, func() (conflictErr error) {
_, conflictErr = controllerutil.CreateOrUpdate(ctx, r.Client, ns, func() error {
metaStatus, err = r.reconcileMetadata(ctx, ns, tnt, stat)
return err
})
return conflictErr
})
r.emitEvent(tnt, namespace, res, "Ensuring Namespace metadata", err)
return err
}
//nolint:nestif
func (r *Manager) reconcileMetadata(
ctx context.Context,
ns *corev1.Namespace,
tnt *capsulev1beta2.Tenant,
stat *capsulev1beta2.TenantStatusNamespaceItem,
) (
managed *capsulev1beta2.TenantStatusNamespaceMetadata,
err error,
) {
capsuleLabel, _ := utils.GetTypeLabel(&capsulev1beta2.Tenant{})
originLabels := ns.GetLabels()
if originLabels == nil {
originLabels = make(map[string]string)
}
originAnnotations := ns.GetAnnotations()
if originAnnotations == nil {
originAnnotations = make(map[string]string)
}
managedAnnotations := buildNamespaceAnnotationsForTenant(tnt)
managedLabels := buildNamespaceLabelsForTenant(tnt)
if opts := tnt.Spec.NamespaceOptions; opts != nil && len(opts.AdditionalMetadataList) > 0 {
for _, md := range opts.AdditionalMetadataList {
var ok bool
ok, err = utils.IsNamespaceSelectedBySelector(ns, md.NamespaceSelector)
if err != nil {
return managed, err
}
if !ok {
continue
}
applyTemplateMap(md.Labels, tnt, ns)
applyTemplateMap(md.Annotations, tnt, ns)
utils.MapMergeNoOverrite(managedLabels, md.Labels)
utils.MapMergeNoOverrite(managedAnnotations, md.Annotations)
}
}
managedMetadataOnly := tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.ManagedMetadataOnly
// Handle User-Defined Metadata, if allowed
if !managedMetadataOnly {
if originLabels != nil {
maps.Copy(originLabels, managedLabels)
}
if originAnnotations != nil {
maps.Copy(originAnnotations, managedAnnotations)
}
// Cleanup old Metadata
instance := tnt.Status.GetInstance(stat)
if instance != nil && instance.Metadata != nil {
for label := range instance.Metadata.Labels {
if _, ok := managedLabels[label]; ok {
continue
}
delete(originLabels, label)
}
for annotation := range instance.Metadata.Annotations {
if _, ok := managedAnnotations[annotation]; ok {
continue
}
delete(originAnnotations, annotation)
}
}
managed = &capsulev1beta2.TenantStatusNamespaceMetadata{
Labels: managedLabels,
Annotations: managedAnnotations,
}
} else {
originLabels = managedLabels
originAnnotations = managedAnnotations
}
originLabels["kubernetes.io/metadata.name"] = ns.GetName()
originLabels[capsuleLabel] = tnt.GetName()
ns.SetLabels(originLabels)
ns.SetAnnotations(originAnnotations)
return managed, err
}
func buildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]string {
annotations := make(map[string]string)
@@ -120,6 +282,35 @@ func buildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]s
return annotations
}
func buildNamespaceLabelsForTenant(tnt *capsulev1beta2.Tenant) map[string]string {
labels := make(map[string]string)
if md := tnt.Spec.NamespaceOptions; md != nil && md.AdditionalMetadata != nil {
maps.Copy(labels, md.AdditionalMetadata.Labels)
}
if tnt.Spec.Cordoned {
labels[meta.CordonedLabel] = "true"
}
return labels
}
func (r *Manager) collectNamespaces(ctx context.Context, tenant *capsulev1beta2.Tenant) (err error) {
list := &corev1.NamespaceList{}
err = r.List(ctx, list, client.MatchingFieldsSelector{
Selector: fields.OneTermEqualSelector(".metadata.ownerReferences[*].capsule", tenant.GetName()),
})
if err != nil {
return err
}
tenant.AssignNamespaces(list.Items)
return err
}
// applyTemplateMap applies templating to all values in the provided map in place.
func applyTemplateMap(m map[string]string, tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) {
for k, v := range m {
@@ -136,99 +327,3 @@ func applyTemplateMap(m map[string]string, tnt *capsulev1beta2.Tenant, ns *corev
m[k] = tmplString
}
}
func buildNamespaceLabelsForTenant(tnt *capsulev1beta2.Tenant) map[string]string {
labels := make(map[string]string)
if md := tnt.Spec.NamespaceOptions; md != nil && md.AdditionalMetadata != nil {
maps.Copy(labels, md.AdditionalMetadata.Labels)
}
return labels
}
func (r *Manager) ensureNamespaceCount(ctx context.Context, tenant *capsulev1beta2.Tenant) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
tenant.Status.Size = uint(len(tenant.Status.Namespaces))
found := &capsulev1beta2.Tenant{}
if err := r.Get(ctx, types.NamespacedName{Name: tenant.GetName()}, found); err != nil {
return err
}
found.Status.Size = tenant.Status.Size
return r.Client.Status().Update(ctx, found, &client.SubResourceUpdateOptions{})
})
}
func (r *Manager) collectNamespaces(ctx context.Context, tenant *capsulev1beta2.Tenant) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
list := &corev1.NamespaceList{}
err = r.List(ctx, list, client.MatchingFieldsSelector{
Selector: fields.OneTermEqualSelector(".metadata.ownerReferences[*].capsule", tenant.GetName()),
})
if err != nil {
return
}
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, tenant.DeepCopy(), func() error {
tenant.AssignNamespaces(list.Items)
return r.Client.Status().Update(ctx, tenant, &client.SubResourceUpdateOptions{})
})
return
})
}
// SyncNamespaceMetadata sync namespace metadata according to tenant spec.
func SyncNamespaceMetadata(tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) error {
capsuleLabel, _ := utils.GetTypeLabel(&capsulev1beta2.Tenant{})
annotations := buildNamespaceAnnotationsForTenant(tnt)
labels := buildNamespaceLabelsForTenant(tnt)
if opts := tnt.Spec.NamespaceOptions; opts != nil && len(opts.AdditionalMetadataList) > 0 {
for _, md := range opts.AdditionalMetadataList {
ok, err := utils.IsNamespaceSelectedBySelector(ns, md.NamespaceSelector)
if err != nil {
return err
}
if !ok {
continue
}
applyTemplateMap(md.Labels, tnt, ns)
applyTemplateMap(md.Annotations, tnt, ns)
maps.Copy(labels, md.Labels)
maps.Copy(annotations, md.Annotations)
}
}
labels["kubernetes.io/metadata.name"] = ns.GetName()
labels[capsuleLabel] = tnt.GetName()
if tnt.Spec.Cordoned {
ns.Labels[utils.CordonedLabel] = "true"
} else {
delete(ns.Labels, utils.CordonedLabel)
}
if ns.Annotations == nil {
ns.SetAnnotations(annotations)
} else {
maps.Copy(ns.Annotations, annotations)
}
if ns.Labels == nil {
ns.SetLabels(labels)
} else {
maps.Copy(ns.Labels, labels)
}
return nil
}

View File

@@ -39,7 +39,6 @@ import (
//
// In case of Namespace-scoped Resource Budget, we're just replicating the resources across all registered Namespaces.
//nolint:nakedret
func (r *Manager) syncResourceQuotas(ctx context.Context, tenant *capsulev1beta2.Tenant) (err error) { //nolint:gocognit
// getting ResourceQuota labels for the mutateFn
var tenantLabel, typeLabel string
@@ -175,16 +174,16 @@ func (r *Manager) syncResourceQuotas(ctx context.Context, tenant *capsulev1beta2
if scopeErr = r.resourceQuotasUpdate(ctx, name, quantity, toKeep, resourceQuota.Hard[name], list.Items...); scopeErr != nil {
r.Log.Error(scopeErr, "cannot proceed with outer ResourceQuota")
return
return scopeErr
}
}
return
return scopeErr
})
}
// Waiting the update of all ResourceQuotas
if err = group.Wait(); err != nil {
return
return err
}
}
// getting requested ResourceQuota keys
@@ -207,7 +206,6 @@ func (r *Manager) syncResourceQuotas(ctx context.Context, tenant *capsulev1beta2
return group.Wait()
}
//nolint:nakedret
func (r *Manager) syncResourceQuota(ctx context.Context, tenant *capsulev1beta2.Tenant, namespace string, keys []string) (err error) {
// getting ResourceQuota labels for the mutateFn
var tenantLabel, typeLabel string
@@ -264,7 +262,7 @@ func (r *Manager) syncResourceQuota(ctx context.Context, tenant *capsulev1beta2.
r.Log.Info("Resource Quota sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
if err != nil {
return
return err
}
}
@@ -295,7 +293,7 @@ func (r *Manager) resourceQuotasUpdate(ctx context.Context, resourceName corev1.
group.Go(func() (err error) {
found := &corev1.ResourceQuota{}
if err = r.Get(ctx, types.NamespacedName{Namespace: rq.Namespace, Name: rq.Name}, found); err != nil {
return
return err
}
return retry.RetryOnConflict(retry.DefaultBackoff, func() (retryErr error) {
@@ -305,12 +303,19 @@ func (r *Manager) resourceQuotasUpdate(ctx context.Context, resourceName corev1.
if found.Annotations == nil {
found.Annotations = make(map[string]string)
}
if found.Labels == nil {
found.Labels = make(map[string]string, len(rq.Labels))
}
// Pruning the Capsule quota annotations:
// if the ResourceQuota is updated by removing some objects,
// we could still have left-overs which could be misleading.
// This will not lead to a reconciliation loop since the whole code is idempotent.
for k := range found.Annotations {
if (strings.HasPrefix(k, capsulev1beta2.HardCapsuleQuotaAnnotation) || strings.HasPrefix(k, capsulev1beta2.UsedCapsuleQuotaAnnotation)) && !annotationsToKeep.Has(k) {
if (strings.HasPrefix(k, capsulev1beta2.HardCapsuleQuotaAnnotation) ||
strings.HasPrefix(k, capsulev1beta2.UsedCapsuleQuotaAnnotation)) &&
(annotationsToKeep == nil || !annotationsToKeep.Has(k)) {
delete(found.Annotations, k)
}
}
@@ -323,8 +328,14 @@ func (r *Manager) resourceQuotasUpdate(ctx context.Context, resourceName corev1.
if limitKey, keyErr := capsulev1beta2.HardQuotaFor(resourceName); keyErr == nil {
found.Annotations[limitKey] = limit.String()
}
// Updating the Resource according to the actual.Cmp result
found.Spec.Hard = rq.Spec.Hard
if rq.Spec.Hard != nil {
found.Spec.Hard = rq.Spec.Hard.DeepCopy()
} else {
// Ensure its nil (or empty) consistently
found.Spec.Hard = nil
}
return nil
})

View File

@@ -71,7 +71,7 @@ func (r *Manager) syncCustomResourceQuotaUsages(ctx context.Context, tenant *cap
err := retry.RetryOnConflict(retry.DefaultBackoff, func() (retryErr error) {
tnt := &capsulev1beta2.Tenant{}
if retryErr = r.Get(ctx, types.NamespacedName{Name: tenant.GetName()}, tnt); retryErr != nil {
return
return retryErr
}
if tnt.GetAnnotations() == nil {
@@ -123,7 +123,7 @@ func (r *Manager) syncCustomResourceQuotaUsages(ctx context.Context, tenant *cap
usedMap[key] += used
}
return
return scopeErr
})
}

View File

@@ -45,6 +45,8 @@ func (r *Manager) ownerClusterRoleBindings(owner capsulev1beta2.OwnerSpec, clust
Subjects: []rbacv1.Subject{
subject,
},
Labels: owner.Labels,
Annotations: owner.Annotations,
}
}
@@ -91,20 +93,19 @@ func (r *Manager) syncRoleBindings(ctx context.Context, tenant *capsulev1beta2.T
return group.Wait()
}
//nolint:nakedret
func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsulev1beta2.Tenant, ns string, keys []string, hashFn func(binding api.AdditionalRoleBindingsSpec) string) (err error) {
var tenantLabel, roleBindingLabel string
if tenantLabel, err = utils.GetTypeLabel(&capsulev1beta2.Tenant{}); err != nil {
return
return err
}
if roleBindingLabel, err = utils.GetTypeLabel(&rbacv1.RoleBinding{}); err != nil {
return
return err
}
if err = r.pruningResources(ctx, ns, keys, &rbacv1.RoleBinding{}); err != nil {
return
return err
}
var roleBindings []api.AdditionalRoleBindingsSpec
@@ -130,17 +131,26 @@ func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsule
var res controllerutil.OperationResult
res, err = controllerutil.CreateOrUpdate(ctx, r.Client, target, func() error {
if target.Labels == nil {
target.Labels = map[string]string{}
target.Labels = map[string]string{}
target.Annotations = map[string]string{}
if roleBinding.Labels != nil {
target.Labels = roleBinding.Labels
}
target.Labels[tenantLabel] = tenant.Name
target.Labels[roleBindingLabel] = roleBindingHashLabel
if roleBinding.Annotations != nil {
target.Annotations = roleBinding.Annotations
}
target.RoleRef = rbacv1.RoleRef{
APIGroup: rbacv1.GroupName,
Kind: "ClusterRole",
Name: roleBinding.ClusterRoleName,
}
target.Subjects = roleBinding.Subjects
return controllerutil.SetControllerReference(tenant, target, r.Scheme())
@@ -155,7 +165,7 @@ func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsule
r.Log.Info(fmt.Sprintf("RoleBinding sync result: %s", string(res)), "name", target.Name, "namespace", target.Namespace)
if err != nil {
return
return err
}
}

View File

@@ -23,7 +23,7 @@ func (r *Manager) pruningResources(ctx context.Context, ns string, keys []string
var capsuleLabel string
if capsuleLabel, err = capsulev1beta2.GetTypeLabel(obj); err != nil {
return
return err
}
selector := labels.NewSelector()
@@ -31,7 +31,7 @@ func (r *Manager) pruningResources(ctx context.Context, ns string, keys []string
var exists *labels.Requirement
if exists, err = labels.NewRequirement(capsuleLabel, selection.Exists, []string{}); err != nil {
return
return err
}
selector = selector.Add(*exists)

View File

@@ -10,8 +10,10 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
@@ -23,7 +25,9 @@ type Patch struct {
Value string `json:"value"`
}
var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
var _ = Describe("enforcing a Container Registry", Label("tenant", "images", "registry"), func() {
originConfig := &capsulev1beta2.CapsuleConfiguration{}
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "container-registry",
@@ -43,13 +47,27 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
}
JustBeforeEach(func() {
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: defaultConfigurationName}, originConfig)).To(Succeed())
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
// Restore Configuration
Eventually(func() error {
c := &capsulev1beta2.CapsuleConfiguration{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: originConfig.Name}, c); err != nil {
return err
}
// Apply the initial configuration from originConfig to c
c.Spec = originConfig.Spec
return k8sClient.Update(context.Background(), c)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
It("should add labels to Namespace", func() {
@@ -71,7 +89,6 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
It("should deny running a gcr.io container", func() {
ns := NewNamespace("")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
@@ -86,14 +103,21 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
},
},
}
cs := ownerClient(tnt.Spec.Owners[0])
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
Expect(err).ShouldNot(Succeed())
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
EventuallyCreation(func() error {
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
return err
}).ShouldNot(Succeed())
})
It("should allow using a registry only match", func() {
ns := NewNamespace("")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
@@ -110,10 +134,26 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
}
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
EventuallyCreation(func() error {
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
return err
}).Should(Succeed())
By("verifying the image was correctly mutated", func() {
created := &corev1.Pod{}
Expect(k8sClient.Get(context.Background(), types.NamespacedName{
Namespace: ns.Name,
Name: pod.Name,
}, created)).To(Succeed())
Expect(created.Spec.Containers).To(HaveLen(1))
Expect(created.Spec.Containers[0].Image).To(Equal("myregistry.azurecr.io/myapp:latest"))
})
})
It("should deny patching a not matching registry after applying with a matching (Container)", func() {
@@ -144,6 +184,17 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
return err
}).Should(Succeed())
By("verifying the image was correctly mutated", func() {
created := &corev1.Pod{}
Expect(k8sClient.Get(context.Background(), types.NamespacedName{
Namespace: ns.Name,
Name: pod.Name,
}, created)).To(Succeed())
Expect(created.Spec.Containers).To(HaveLen(1))
Expect(created.Spec.Containers[0].Image).To(Equal("myregistry.azurecr.io/myapp:latest"))
})
Eventually(func() error {
payload := []Patch{{
Op: "replace",
@@ -159,6 +210,89 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
}).ShouldNot(Succeed())
})
It("should deny patching a not matching registry after applying with a matching (EphemeralContainer)", func() {
ns := NewNamespace("")
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "container",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "container",
Image: "docker.io/google-containers/pause-amd64:3.0",
},
},
},
}
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: "ephemeralcontainers-editor",
Namespace: ns.GetName(),
},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{""}, // core API group
Resources: []string{"pods/ephemeralcontainers"},
Verbs: []string{"update", "patch"},
},
},
}
rb := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "ephemeralcontainers-editor-binding",
Namespace: ns.GetName(),
},
Subjects: []rbacv1.Subject{
{
Kind: rbacv1.UserKind,
Name: tnt.Spec.Owners[0].Name,
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "Role",
Name: role.Name,
},
}
// Create role and binding before test logic
Expect(k8sClient.Create(context.TODO(), role)).To(Succeed())
Expect(k8sClient.Create(context.TODO(), rb)).To(Succeed())
EventuallyCreation(func() error {
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
return err
}).Should(Succeed())
Eventually(func() error {
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "dbg",
Image: "attacker/google-containers/pause-amd64:3.0",
ImagePullPolicy: corev1.PullIfNotPresent,
},
},
}
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
if err != nil {
return err
}
return nil
}).ShouldNot(Succeed())
})
It("should deny patching a not matching registry after applying with a matching (initContainer)", func() {
ns := NewNamespace("")
@@ -208,7 +342,50 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
}).ShouldNot(Succeed())
})
It("should allow patching a matching registry after applying with a matching (Container)", func() {
It("should deny patching a not matching registry after applying with a matching (Container)", func() {
ns := NewNamespace("")
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "container",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "container",
Image: "myregistry.azurecr.io/myapp:latest",
},
},
},
}
cs := ownerClient(tnt.Spec.Owners[0])
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
EventuallyCreation(func() error {
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
return err
}).Should(Succeed())
Eventually(func() error {
payload := []Patch{{
Op: "replace",
Path: "/spec/initContainers/0/image",
Value: "attacker/google-containers/pause-amd64:3.0",
}}
payloadBytes, _ := json.Marshal(payload)
_, err := cs.CoreV1().Pods(ns.GetName()).Patch(context.TODO(), pod.GetName(), types.JSONPatchType, payloadBytes, metav1.PatchOptions{})
if err != nil {
return err
}
return nil
}).ShouldNot(Succeed())
})
It("should allow patching a matching registry after applying with a matching (EphemeralContainer)", func() {
ns := NewNamespace("")
pod := &corev1.Pod{
@@ -230,6 +407,42 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: "ephemeralcontainers-editor",
Namespace: ns.GetName(),
},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{""}, // core API group
Resources: []string{"pods/ephemeralcontainers"},
Verbs: []string{"update", "patch"},
},
},
}
rb := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "ephemeralcontainers-editor-binding",
Namespace: ns.GetName(),
},
Subjects: []rbacv1.Subject{
{
Kind: rbacv1.UserKind,
Name: tnt.Spec.Owners[0].Name,
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "Role",
Name: role.Name,
},
}
// Create role and binding before test logic
Expect(k8sClient.Create(context.TODO(), role)).To(Succeed())
Expect(k8sClient.Create(context.TODO(), rb)).To(Succeed())
EventuallyCreation(func() error {
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
@@ -237,13 +450,17 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
}).Should(Succeed())
Eventually(func() error {
payload := []Patch{{
Op: "replace",
Path: "/spec/containers/0/image",
Value: "myregistry.azurecr.io/google-containers/pause-amd64:3.1",
}}
payloadBytes, _ := json.Marshal(payload)
_, err := cs.CoreV1().Pods(ns.GetName()).Patch(context.TODO(), pod.GetName(), types.JSONPatchType, payloadBytes, metav1.PatchOptions{})
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "dbg",
Image: "myregistry.azurecr.io/google-containers/pause-amd64:3.1",
ImagePullPolicy: corev1.PullIfNotPresent,
},
},
}
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
if err != nil {
return err
}

View File

@@ -9,13 +9,14 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
)
var _ = Describe("enforcing some defined ImagePullPolicy", Label("pod"), func() {
var _ = Describe("enforcing some defined ImagePullPolicy", Label("tenant", "images", "policy"), func() {
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "image-pull-policies",
@@ -48,6 +49,42 @@ var _ = Describe("enforcing some defined ImagePullPolicy", Label("pod"), func()
cs := ownerClient(tnt.Spec.Owners[0])
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: "ephemeralcontainers-editor",
Namespace: ns.GetName(),
},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{""}, // core API group
Resources: []string{"pods/ephemeralcontainers"},
Verbs: []string{"update", "patch"},
},
},
}
rb := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "ephemeralcontainers-editor-binding",
Namespace: ns.GetName(),
},
Subjects: []rbacv1.Subject{
{
Kind: rbacv1.UserKind,
Name: tnt.Spec.Owners[0].Name,
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "Role",
Name: role.Name,
},
}
// Create role and binding before test logic
Expect(k8sClient.Create(context.TODO(), role)).To(Succeed())
Expect(k8sClient.Create(context.TODO(), rb)).To(Succeed())
By("allowing Always", func() {
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
@@ -69,6 +106,25 @@ var _ = Describe("enforcing some defined ImagePullPolicy", Label("pod"), func()
return
}).Should(Succeed())
Eventually(func() error {
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "dbg",
Image: "gcr.io/google_containers/pause-amd64:3.0",
ImagePullPolicy: corev1.PullAlways,
},
},
}
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
if err != nil {
return err
}
return nil
}).Should(Succeed())
})
By("allowing IfNotPresent", func() {
@@ -92,6 +148,24 @@ var _ = Describe("enforcing some defined ImagePullPolicy", Label("pod"), func()
return
}).Should(Succeed())
Eventually(func() error {
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "dbg",
Image: "gcr.io/google_containers/pause-amd64:3.0",
ImagePullPolicy: corev1.PullIfNotPresent,
},
},
}
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
if err != nil {
return err
}
return nil
}).Should(Succeed())
})
By("blocking Never", func() {
@@ -115,6 +189,25 @@ var _ = Describe("enforcing some defined ImagePullPolicy", Label("pod"), func()
return
}).ShouldNot(Succeed())
Eventually(func() error {
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "dbg",
Image: "gcr.io/google_containers/pause-amd64:3.0",
ImagePullPolicy: corev1.PullNever,
},
},
}
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
if err != nil {
return err
}
return nil
}).ShouldNot(Succeed())
})
})
})

View File

@@ -9,13 +9,14 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
)
var _ = Describe("enforcing a defined ImagePullPolicy", Label("pod"), func() {
var _ = Describe("enforcing a defined ImagePullPolicy", Label("tenant", "images", "policy"), func() {
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "image-pull-policy",
@@ -48,6 +49,42 @@ var _ = Describe("enforcing a defined ImagePullPolicy", Label("pod"), func() {
cs := ownerClient(tnt.Spec.Owners[0])
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: "ephemeralcontainers-editor",
Namespace: ns.GetName(),
},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{""}, // core API group
Resources: []string{"pods/ephemeralcontainers"},
Verbs: []string{"update", "patch"},
},
},
}
rb := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "ephemeralcontainers-editor-binding",
Namespace: ns.GetName(),
},
Subjects: []rbacv1.Subject{
{
Kind: rbacv1.UserKind,
Name: tnt.Spec.Owners[0].Name,
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "Role",
Name: role.Name,
},
}
// Create role and binding before test logic
Expect(k8sClient.Create(context.TODO(), role)).To(Succeed())
Expect(k8sClient.Create(context.TODO(), rb)).To(Succeed())
By("allowing Always", func() {
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
@@ -69,6 +106,24 @@ var _ = Describe("enforcing a defined ImagePullPolicy", Label("pod"), func() {
return
}).Should(Succeed())
Eventually(func() error {
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "dbg",
Image: "gcr.io/google_containers/pause-amd64:3.0",
ImagePullPolicy: corev1.PullAlways,
},
},
}
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
if err != nil {
return err
}
return nil
}).Should(Succeed())
})
By("blocking IfNotPresent", func() {
@@ -92,6 +147,24 @@ var _ = Describe("enforcing a defined ImagePullPolicy", Label("pod"), func() {
return
}).ShouldNot(Succeed())
Eventually(func() error {
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "dbg",
Image: "gcr.io/google_containers/pause-amd64:3.0",
ImagePullPolicy: corev1.PullIfNotPresent,
},
},
}
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
if err != nil {
return err
}
return nil
}).ShouldNot(Succeed())
})
By("blocking Never", func() {
@@ -115,6 +188,24 @@ var _ = Describe("enforcing a defined ImagePullPolicy", Label("pod"), func() {
return
}).ShouldNot(Succeed())
Eventually(func() error {
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "dbg",
Image: "gcr.io/google_containers/pause-amd64:3.0",
ImagePullPolicy: corev1.PullNever,
},
},
}
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
if err != nil {
return err
}
return nil
}).ShouldNot(Succeed())
})
})
})

View File

@@ -8,14 +8,17 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/meta"
)
var _ = Describe("creating a Namespace for a Tenant with additional metadata", Label("namespace"), func() {
var _ = Describe("creating a Namespace for a Tenant with additional metadata", Label("namespace", "metadata"), func() {
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-metadata",
@@ -35,7 +38,16 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
Kind: "User",
},
},
NamespaceOptions: &capsulev1beta2.NamespaceOptions{
},
}
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
It("should contain additional Namespace metadata", func() {
By("prepare tenant", func() {
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
AdditionalMetadata: &api.AdditionalMetadataSpec{
Labels: map[string]string{
"k8s.io/custom-label": "foo",
@@ -48,20 +60,16 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
"clastix.io/custom-annotation": "buzz",
},
},
},
},
}
}
JustBeforeEach(func() {
EventuallyCreation(func() error {
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
})
It("should contain additional Namespace metadata", func() {
ns := NewNamespace("")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
@@ -105,29 +113,10 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
})
})
})
var _ = Describe("creating a Namespace for a Tenant with additional metadata list", func() {
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-metadata",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "cap",
Kind: "dummy",
Name: "tenant-metadata",
UID: "tenant-metadata",
},
},
},
Spec: capsulev1beta2.TenantSpec{
Owners: capsulev1beta2.OwnerListSpec{
{
Name: "gatsby",
Kind: "User",
},
},
NamespaceOptions: &capsulev1beta2.NamespaceOptions{
It("should contain additional Namespace metadata", func() {
By("prepare tenant", func() {
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{
{
Labels: map[string]string{
@@ -184,20 +173,16 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata lis
},
},
},
},
},
}
}
JustBeforeEach(func() {
EventuallyCreation(func() error {
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
})
It("should contain additional Namespace metadata", func() {
labels := map[string]string{
"matching_namespace_label": "matching_namespace_label_value",
}
@@ -295,6 +280,434 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata lis
return
}, defaultTimeoutInterval, defaultPollInterval).Should(BeFalse())
})
})
It("should contain additional Namespace metadata", func() {
By("prepare tenant", func() {
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
ManagedMetadataOnly: false,
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{
{
Labels: map[string]string{
"clastix.io/custom-label": "bar",
},
Annotations: map[string]string{
"clastix.io/custom-annotation": "buzz",
},
},
{
Labels: map[string]string{
"k8s.io/custom-label": "foo",
},
Annotations: map[string]string{
"k8s.io/custom-annotation": "bizz",
},
},
},
}
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
})
labels := map[string]string{
"matching_namespace_label": "matching_namespace_label_value",
}
ns := NewNamespace("", labels)
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
By("checking additional labels", func() {
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
for _, mv := range tnt.Spec.NamespaceOptions.AdditionalMetadataList {
for k, v := range mv.Labels {
if k == "capsule.clastix.io/tenant" || k == "kubernetes.io/metadata.name" {
continue // this label is managed and shouldn't be set by the user
}
if ok, _ = HaveKeyWithValue(k, v).Match(ns.Labels); !ok {
return
}
}
}
return
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
})
By("checking managed labels", func() {
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
if ok, _ = HaveKeyWithValue("capsule.clastix.io/tenant", tnt.GetName()).Match(ns.Labels); !ok {
return
}
if ok, _ = HaveKeyWithValue("kubernetes.io/metadata.name", ns.GetName()).Match(ns.Labels); !ok {
return
}
return
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
})
By("checking additional annotations", func() {
Eventually(func() (ok bool) {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
for _, mv := range tnt.Spec.NamespaceOptions.AdditionalMetadataList {
for k, v := range mv.Annotations {
if ok, _ = HaveKeyWithValue(k, v).Match(ns.Annotations); !ok {
return
}
}
}
return
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
})
By("patching labels and annotations on the Namespace", func() {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).To(Succeed())
before := ns.DeepCopy()
ns.Labels["test-label"] = "test-value"
ns.Labels["k8s.io/custom-label"] = "foo-value"
ns.Annotations["test-annotation"] = "test-value"
ns.Annotations["k8s.io/custom-annotation"] = "bizz-value"
Expect(k8sClient.Patch(context.TODO(), ns, client.MergeFrom(before))).To(Succeed())
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
})
By("Add additional annotations (Tenant Owner)", func() {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
expectedLabels := map[string]string{
"test-label": "test-value",
"clastix.io/custom-label": "bar",
"k8s.io/custom-label": "foo",
"matching_namespace_label": "matching_namespace_label_value",
"capsule.clastix.io/tenant": tnt.GetName(),
"kubernetes.io/metadata.name": ns.GetName(),
}
Eventually(func() map[string]string {
got := &corev1.Namespace{}
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
return nil
}
ann := got.GetLabels()
if ann == nil {
ann = map[string]string{}
}
return ann
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedLabels))
expectedAnnotations := map[string]string{
"test-annotation": "test-value",
"clastix.io/custom-annotation": "buzz",
"k8s.io/custom-annotation": "bizz",
}
Eventually(func() map[string]string {
got := &corev1.Namespace{}
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
return nil
}
ann := got.GetAnnotations()
if ann == nil {
ann = map[string]string{}
}
return ann
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedAnnotations))
By("verify tenant status", func() {
condition := tnt.Status.Conditions.GetConditionByType(meta.ReadyCondition)
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected tenant condition status to be True")
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected tenant condition type to be Ready")
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected tenant condition reason to be Succeeded")
})
By("verify namespace status", func() {
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns.GetName(), UID: ns.GetUID()})
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
Expect(instance.Name).To(Equal(ns.GetName()))
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected namespace condition status to be True")
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
expectedMetadata := &capsulev1beta2.TenantStatusNamespaceMetadata{
Labels: map[string]string{
"clastix.io/custom-label": "bar",
"k8s.io/custom-label": "foo",
},
Annotations: map[string]string{
"clastix.io/custom-annotation": "buzz",
"k8s.io/custom-annotation": "bizz",
},
}
Expect(instance.Metadata).To(Equal(expectedMetadata))
})
})
By("change managed additional metadata", func() {
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
ManagedMetadataOnly: false,
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{
{
Labels: map[string]string{
"clastix.io/custom-label": "bar",
},
},
{
Annotations: map[string]string{
"k8s.io/custom-annotation": "bizz",
},
},
},
}
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
})
By("verify metadata lifecycle (valid update)", func() {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).To(Succeed())
expectedLabels := map[string]string{
"test-label": "test-value",
"clastix.io/custom-label": "bar",
"matching_namespace_label": "matching_namespace_label_value",
"capsule.clastix.io/tenant": tnt.GetName(),
"kubernetes.io/metadata.name": ns.GetName(),
}
Eventually(func() map[string]string {
got := &corev1.Namespace{}
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
return nil
}
ann := got.GetLabels()
if ann == nil {
ann = map[string]string{}
}
return ann
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedLabels))
expectedAnnotations := map[string]string{
"test-annotation": "test-value",
"k8s.io/custom-annotation": "bizz",
}
Eventually(func() map[string]string {
got := &corev1.Namespace{}
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
return nil
}
ann := got.GetAnnotations()
if ann == nil {
ann = map[string]string{}
}
return ann
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedAnnotations))
By("verify tenant status", func() {
condition := tnt.Status.Conditions.GetConditionByType(meta.ReadyCondition)
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected tenant condition status to be True")
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected tenant condition type to be Ready")
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected tenant condition reason to be Succeeded")
})
By("verify namespace status", func() {
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns.GetName(), UID: ns.GetUID()})
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
Expect(instance.Name).To(Equal(ns.GetName()))
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected namespace condition status to be True")
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
expectedMetadata := &capsulev1beta2.TenantStatusNamespaceMetadata{
Labels: map[string]string{
"clastix.io/custom-label": "bar",
},
Annotations: map[string]string{
"k8s.io/custom-annotation": "bizz",
},
}
Expect(instance.Metadata).To(Equal(expectedMetadata))
})
})
By("change managed additional metadata (provoke an error)", func() {
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
ManagedMetadataOnly: false,
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{
{
Labels: map[string]string{
"clastix.io???custom-label": "bar",
},
},
},
}
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
})
By("verify metadata lifecycle (faulty update)", func() {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).To(Succeed())
expectedLabels := map[string]string{
"test-label": "test-value",
"clastix.io/custom-label": "bar",
"matching_namespace_label": "matching_namespace_label_value",
"capsule.clastix.io/tenant": tnt.GetName(),
"kubernetes.io/metadata.name": ns.GetName(),
}
Eventually(func() map[string]string {
got := &corev1.Namespace{}
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
return nil
}
ann := got.GetLabels()
if ann == nil {
ann = map[string]string{}
}
return ann
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedLabels))
expectedAnnotations := map[string]string{
"test-annotation": "test-value",
"k8s.io/custom-annotation": "bizz",
}
Eventually(func() map[string]string {
got := &corev1.Namespace{}
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
return nil
}
ann := got.GetAnnotations()
if ann == nil {
ann = map[string]string{}
}
return ann
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedAnnotations))
By("verify tenant status", func() {
condition := tnt.Status.Conditions.GetConditionByType(meta.ReadyCondition)
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
Expect(condition.Status).To(Equal(metav1.ConditionFalse), "Expected tenant condition status to be True")
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected tenant condition type to be Ready")
Expect(condition.Reason).To(Equal(meta.FailedReason), "Expected tenant condition reason to be Succeeded")
})
By("verify namespace status", func() {
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns.GetName(), UID: ns.GetUID()})
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
Expect(instance.Name).To(Equal(ns.GetName()))
Expect(condition.Status).To(Equal(metav1.ConditionFalse), "Expected namespace condition status to be True")
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
Expect(condition.Reason).To(Equal(meta.FailedReason), "Expected namespace condition reason to be Succeeded")
expectedMetadata := &capsulev1beta2.TenantStatusNamespaceMetadata{
Labels: map[string]string{
"clastix.io/custom-label": "bar",
},
Annotations: map[string]string{
"k8s.io/custom-annotation": "bizz",
},
}
Expect(instance.Metadata).To(Equal(expectedMetadata))
})
})
By("change managed additional metadata (empty update)", func() {
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
ManagedMetadataOnly: false,
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{},
}
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
})
By("verify metadata lifecycle (empty update)", func() {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).To(Succeed())
expectedLabels := map[string]string{
"test-label": "test-value",
"matching_namespace_label": "matching_namespace_label_value",
"capsule.clastix.io/tenant": tnt.GetName(),
"kubernetes.io/metadata.name": ns.GetName(),
}
Eventually(func() map[string]string {
got := &corev1.Namespace{}
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
return nil
}
ann := got.GetLabels()
if ann == nil {
ann = map[string]string{}
}
return ann
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedLabels))
expectedAnnotations := map[string]string{
"test-annotation": "test-value",
}
Eventually(func() map[string]string {
got := &corev1.Namespace{}
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
return nil
}
ann := got.GetAnnotations()
if ann == nil {
ann = map[string]string{}
}
return ann
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedAnnotations))
By("verify tenant status", func() {
condition := tnt.Status.Conditions.GetConditionByType(meta.ReadyCondition)
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected tenant condition status to be True")
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected tenant condition type to be Ready")
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected tenant condition reason to be Succeeded")
})
By("verify namespace status", func() {
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns.GetName(), UID: ns.GetUID()})
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
Expect(instance.Name).To(Equal(ns.GetName()))
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected namespace condition status to be True")
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
expectedMetadata := &capsulev1beta2.TenantStatusNamespaceMetadata{}
Expect(instance.Metadata).To(Equal(expectedMetadata))
})
})
})
})

View File

@@ -18,7 +18,7 @@ import (
var _ = Describe("creating a Namespace for a Tenant with additional metadata", Label("namespace"), func() {
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-metadata",
Name: "tenant-metadata-controller",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "cap",
@@ -68,6 +68,7 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
Expect(ns.Labels).ShouldNot(HaveKeyWithValue("newlabel", "foobazbar"))
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
tnt.Spec.NamespaceOptions.AdditionalMetadata.Labels["newlabel"] = "foobazbar"
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
@@ -81,6 +82,7 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
Expect(ns.Labels).ShouldNot(HaveKeyWithValue("newannotation", "foobazbar"))
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
tnt.Spec.NamespaceOptions.AdditionalMetadata.Annotations["newannotation"] = "foobazbar"
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())

View File

@@ -20,7 +20,7 @@ import (
var _ = Describe("creating a Namespace for a Tenant with additional metadata", Label("namespace"), func() {
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-metadata",
Name: "tenant-metadata-webhook",
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "cap",

View File

@@ -0,0 +1,112 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/meta"
)
var _ = Describe("creating namespace with status lifecycle", Label("namespace", "status"), func() {
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-status",
},
Spec: capsulev1beta2.TenantSpec{
Owners: capsulev1beta2.OwnerListSpec{
{
Name: "gatsby",
Kind: "User",
},
},
},
}
JustBeforeEach(func() {
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
It("verify namespace lifecycle (functionality)", func() {
ns1 := NewNamespace("")
By("creating first namespace", func() {
NamespaceCreation(ns1, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElements(ns1.GetName()))
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
Expect(tnt.Status.Size).To(Equal(uint(1)))
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns1.GetName(), UID: ns1.GetUID()})
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
Expect(instance.Name).To(Equal(ns1.GetName()))
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected namespace condition status to be True")
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
})
ns2 := NewNamespace("")
By("creating second namespace", func() {
NamespaceCreation(ns2, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElements(ns2.GetName()))
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
Expect(tnt.Status.Size).To(Equal(uint(2)))
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns2.GetName(), UID: ns2.GetUID()})
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
Expect(instance.Name).To(Equal(ns2.GetName()))
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected namespace condition status to be True")
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
})
By("removing first namespace", func() {
Expect(k8sClient.Delete(context.TODO(), ns1)).Should(Succeed())
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
Expect(t.Status.Size).To(Equal(uint(1)))
instance := t.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns1.GetName(), UID: ns1.GetUID()})
Expect(instance).To(BeNil(), "Namespace instance should be nil")
})
By("removing second namespace", func() {
Expect(k8sClient.Delete(context.TODO(), ns2)).Should(Succeed())
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
Expect(t.Status.Size).To(Equal(uint(0)))
instance := t.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns2.GetName(), UID: ns2.GetUID()})
Expect(instance).To(BeNil(), "Namespace instance should be nil")
})
})
})

View File

@@ -377,6 +377,16 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
Expect(claim.Status.Pool).To(Equal(expectedPool), "expected pool name to match")
})
By("Error on deleting bound claim", func() {
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
isBoundCondition(claim)
err = k8sClient.Delete(context.TODO(), claim)
Expect(err).ShouldNot(Succeed())
})
By("Error on patching resources for claim (Increase)", func() {
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())

View File

@@ -0,0 +1,353 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
"fmt"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
otypes "github.com/onsi/gomega/types"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
ctrlrbac "github.com/projectcapsule/capsule/controllers/rbac"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/meta"
)
var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label("promotion"), func() {
originConfig := &capsulev1beta2.CapsuleConfiguration{}
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-owner-promotion",
},
Spec: capsulev1beta2.TenantSpec{
Owners: capsulev1beta2.OwnerListSpec{
{
Name: "alice",
Kind: "User",
},
},
AdditionalRoleBindings: []api.AdditionalRoleBindingsSpec{
{
ClusterRoleName: "cluster-admin",
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: "default",
},
{
Kind: "User",
Name: "bob",
},
},
},
},
},
}
JustBeforeEach(func() {
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: defaultConfigurationName}, originConfig)).To(Succeed())
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
// Restore Configuration
Eventually(func() error {
c := &capsulev1beta2.CapsuleConfiguration{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: originConfig.Name}, c); err != nil {
return err
}
// Apply the initial configuration from originConfig to c
c.Spec = originConfig.Spec
return k8sClient.Update(context.Background(), c)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
It("Deny Owner promotion even when feature is disabled", func() {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
configuration.Spec.AllowServiceAccountPromotion = false
})
ns := NewNamespace("")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
time.Sleep(250 * time.Millisecond)
// Create a ServiceAccount inside the tenant namespace
sa := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "test-sa",
Namespace: ns.Name,
},
}
Expect(k8sClient.Create(context.TODO(), sa)).Should(Succeed())
// Table of personas: client + expected result
personas := map[string]struct {
client client.Client
matcher otypes.GomegaMatcher
}{
"owner": {client: impersonationClient(tnt.Spec.Owners[0].Name, withDefaultGroups(make([]string, 0)), k8sClient.Scheme()), matcher: Not(Succeed())},
"rb-user": {client: impersonationClient("bob", withDefaultGroups(make([]string, 0)), k8sClient.Scheme()), matcher: Not(Succeed())},
"rb-sa": {client: impersonationClient("system:serviceaccount:"+sa.GetNamespace()+":default", withDefaultGroups(make([]string, 0)), k8sClient.Scheme()), matcher: Not(Succeed())},
}
for name, tc := range personas {
By(fmt.Sprintf("trying to promote SA as %s (Setting Trigger)", name))
Eventually(func() error {
saCopy := &corev1.ServiceAccount{}
Expect(tc.client.Get(context.TODO(), client.ObjectKeyFromObject(sa), saCopy)).To(Succeed())
if saCopy.Labels == nil {
saCopy.Labels = map[string]string{}
}
saCopy.Labels[meta.OwnerPromotionLabel] = meta.OwnerPromotionLabelTrigger
return tc.client.Update(context.TODO(), saCopy)
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
}
for name, tc := range personas {
By(fmt.Sprintf("trying to promote SA as %s (Setting Any Value)", name))
Eventually(func() error {
saCopy := &corev1.ServiceAccount{}
Expect(tc.client.Get(context.TODO(), client.ObjectKeyFromObject(sa), saCopy)).To(Succeed())
if saCopy.Labels == nil {
saCopy.Labels = map[string]string{}
}
saCopy.Labels[meta.OwnerPromotionLabel] = "false"
return tc.client.Update(context.TODO(), saCopy)
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
}
for name, tc := range personas {
By(fmt.Sprintf("trying to allow deletion SA as %s (Setting Any Value)", name))
Eventually(func() error {
saCopy := &corev1.ServiceAccount{}
Expect(tc.client.Get(context.TODO(), client.ObjectKeyFromObject(sa), saCopy)).To(Succeed())
if saCopy.Labels == nil {
saCopy.Labels = map[string]string{}
}
saCopy.Labels[meta.OwnerPromotionLabel] = "false"
return tc.client.Update(context.TODO(), saCopy)
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
}
for name, tc := range personas {
By(fmt.Sprintf("trying to allow deletion SA as %s (Setting Any Value)", name))
Eventually(func() error {
saCopy := &corev1.ServiceAccount{}
Expect(tc.client.Get(context.TODO(), client.ObjectKeyFromObject(sa), saCopy)).To(Succeed())
if saCopy.Labels == nil {
saCopy.Labels = map[string]string{}
}
saCopy.Labels[meta.OwnerPromotionLabel] = "false"
return tc.client.Update(context.TODO(), saCopy)
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
}
})
It("Allow Owner promotion by Owners", func() {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
configuration.Spec.AllowServiceAccountPromotion = true
})
ns := NewNamespace("")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
time.Sleep(250 * time.Millisecond)
// Create a ServiceAccount inside the tenant namespace
sa := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "test-sa",
Namespace: ns.Name,
},
}
Expect(k8sClient.Create(context.TODO(), sa)).Should(Succeed())
// Table of personas: client + expected result
personas := map[string]struct {
client client.Client
matcher otypes.GomegaMatcher
}{
"rb-user": {client: impersonationClient("bob", withDefaultGroups(make([]string, 0)), k8sClient.Scheme()), matcher: Not(Succeed())},
"rb-sa": {client: impersonationClient("system:serviceaccount:"+sa.GetNamespace()+":default", withDefaultGroups(make([]string, 0)), k8sClient.Scheme()), matcher: Not(Succeed())},
"owner": {client: impersonationClient(tnt.Spec.Owners[0].Name, withDefaultGroups(make([]string, 0)), k8sClient.Scheme()), matcher: Succeed()},
}
for name, tc := range personas {
By(fmt.Sprintf("trying to promote SA as %s (Setting Trigger)", name))
Eventually(func() error {
saCopy := &corev1.ServiceAccount{}
Expect(tc.client.Get(context.TODO(), client.ObjectKeyFromObject(sa), saCopy)).To(Succeed())
if saCopy.Labels == nil {
saCopy.Labels = map[string]string{}
}
saCopy.Labels[meta.OwnerPromotionLabel] = meta.OwnerPromotionLabelTrigger
return tc.client.Update(context.TODO(), saCopy)
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
}
for name, tc := range personas {
By(fmt.Sprintf("trying to promote SA as %s (Setting Generic)", name))
Eventually(func() error {
saCopy := &corev1.ServiceAccount{}
Expect(tc.client.Get(context.TODO(), client.ObjectKeyFromObject(sa), saCopy)).To(Succeed())
if saCopy.Labels == nil {
saCopy.Labels = map[string]string{}
}
saCopy.Labels[meta.OwnerPromotionLabel] = "false"
return tc.client.Update(context.TODO(), saCopy)
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
}
})
It("Allow Promoted ServiceAccount to interact with Tenant Namespaces", func() {
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
configuration.Spec.AllowServiceAccountPromotion = true
})
ns := NewNamespace("")
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
time.Sleep(250 * time.Millisecond)
// Create a ServiceAccount inside the tenant namespace
sa := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "test-sa",
Namespace: ns.Name,
},
}
Expect(k8sClient.Create(context.TODO(), sa)).Should(Succeed())
// Table of personas: client + expected result
personas := map[string]struct {
client client.Client
matcher otypes.GomegaMatcher
}{
"owner": {client: impersonationClient(tnt.Spec.Owners[0].Name, withDefaultGroups(make([]string, 0)), k8sClient.Scheme()), matcher: Succeed()},
}
for name, tc := range personas {
By(fmt.Sprintf("trying to promote SA as %s", name))
Eventually(func() error {
saCopy := &corev1.ServiceAccount{}
Expect(tc.client.Get(context.TODO(), client.ObjectKeyFromObject(sa), saCopy)).To(Succeed())
if saCopy.Labels == nil {
saCopy.Labels = map[string]string{}
}
saCopy.Labels[meta.OwnerPromotionLabel] = meta.OwnerPromotionLabelTrigger
return tc.client.Update(context.TODO(), saCopy)
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
}
time.Sleep(250 * time.Millisecond)
Eventually(func(g Gomega) []rbacv1.Subject {
crb := &rbacv1.ClusterRoleBinding{}
err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ctrlrbac.ProvisionerRoleName}, crb)
g.Expect(err).NotTo(HaveOccurred())
return crb.Subjects
}, defaultTimeoutInterval, defaultPollInterval).Should(ContainElement(rbacv1.Subject{
Kind: rbacv1.ServiceAccountKind,
Name: "test-sa",
Namespace: ns.Name,
}), "expected ServiceAccount test-sa to be present in CRB subjects")
saClient := impersonationClient(
fmt.Sprintf("system:serviceaccount:%s:%s", ns.Name, sa.Name),
nil,
k8sClient.Scheme(),
)
newNs := NewNamespace("")
Expect(saClient.Create(context.TODO(), newNs)).To(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElements(ns.GetName()))
Expect(saClient.Delete(context.TODO(), newNs)).To(Not(Succeed()))
for name, tc := range personas {
By(fmt.Sprintf("trying to promote SA as %s", name))
Eventually(func() error {
saCopy := &corev1.ServiceAccount{}
Expect(tc.client.Get(context.TODO(), client.ObjectKeyFromObject(sa), saCopy)).To(Succeed())
if saCopy.Labels == nil {
saCopy.Labels = map[string]string{}
}
saCopy.Labels[meta.OwnerPromotionLabel] = "false"
return tc.client.Update(context.TODO(), saCopy)
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
Eventually(func() (string, error) {
latest := &corev1.ServiceAccount{}
if err := k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(sa), latest); err != nil {
return "", err
}
return latest.Labels[meta.OwnerPromotionLabel], nil
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal("false"), "expected label to be set for persona=%s", name)
}
Eventually(func(g Gomega) []rbacv1.Subject {
crb := &rbacv1.ClusterRoleBinding{}
err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ctrlrbac.ProvisionerRoleName}, crb)
g.Expect(err).NotTo(HaveOccurred())
return crb.Subjects
}, defaultTimeoutInterval, defaultPollInterval).Should(Not(ContainElement(rbacv1.Subject{
Kind: rbacv1.ServiceAccountKind,
Name: "test-sa",
Namespace: ns.Name,
})), "expected ServiceAccount test-sa not to be present in CRB subjects")
time.Sleep(250 * time.Millisecond)
secondNs := NewNamespace("")
Eventually(func() error {
return saClient.Create(context.TODO(), secondNs)
}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(Not(ContainElements(secondNs.GetName())))
Expect(saClient.Delete(context.TODO(), secondNs)).To(Not(Succeed()))
})
})

136
e2e/scalability_test.go Normal file
View File

@@ -0,0 +1,136 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
"fmt"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/meta"
)
var _ = Describe("verify scalability", Label("scalability"), func() {
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-scalability",
},
Spec: capsulev1beta2.TenantSpec{
Owners: capsulev1beta2.OwnerListSpec{
{
Name: "gatsby",
Kind: "User",
},
},
},
}
JustBeforeEach(func() {
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
It("verify lifecycle (scalability)", func() {
const amount = 50
getTenant := func() *capsulev1beta2.Tenant {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).To(Succeed())
return t
}
waitSize := func(expected uint) {
Eventually(func() uint {
return getTenant().Status.Size
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expected))
}
waitInstancePresent := func(ns *corev1.Namespace) {
Eventually(func() error {
t := getTenant()
inst := t.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{
Name: ns.GetName(),
UID: ns.GetUID(),
})
if inst == nil {
return fmt.Errorf("instance not found for ns=%q uid=%q", ns.GetName(), ns.GetUID())
}
condition := inst.Conditions.GetConditionByType(meta.ReadyCondition)
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
if inst == nil {
return fmt.Errorf("instance not found for ns=%q uid=%q", ns.GetName(), ns.GetUID())
}
if inst.Name != ns.GetName() {
return fmt.Errorf("instance.Name=%q, want %q", inst.Name, ns.GetName())
}
cond := inst.Conditions.GetConditionByType(meta.ReadyCondition)
if cond == nil {
return fmt.Errorf("missing %q condition", meta.ReadyCondition)
}
if cond.Type != meta.ReadyCondition {
return fmt.Errorf("cond.Type=%q, want %q", cond.Type, meta.ReadyCondition)
}
if cond.Status != metav1.ConditionTrue {
return fmt.Errorf("cond.Status=%q, want %q", cond.Status, metav1.ConditionTrue)
}
if cond.Reason != meta.SucceededReason {
return fmt.Errorf("cond.Reason=%q, want %q", cond.Reason, meta.SucceededReason)
}
return nil
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
waitInstanceAbsent := func(ns *corev1.Namespace) {
Eventually(func() bool {
t := getTenant()
inst := t.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{
Name: ns.GetName(),
UID: ns.GetUID(),
})
return inst == nil
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
}
// --- Scale up: create N namespaces and verify Tenant status each time ---
namespaces := make([]*corev1.Namespace, 0, amount)
for i := 0; i < amount; i++ {
ns := NewNamespace(fmt.Sprintf("scale-%d", i))
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
// Expect size bumped to i+1 and instance present
waitSize(uint(i + 1))
waitInstancePresent(ns)
namespaces = append(namespaces, ns)
}
// --- Scale down: delete N namespaces and verify Tenant status each time ---
for i := 0; i < amount; i++ {
ns := namespaces[i]
Expect(k8sClient.Delete(context.TODO(), ns)).To(Succeed())
// Expect size decremented and instance absent
waitSize(uint(amount - i - 1))
waitInstanceAbsent(ns)
}
})
})

View File

@@ -8,6 +8,7 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
@@ -73,3 +74,19 @@ func ownerClient(owner capsulev1beta2.OwnerSpec) (cs kubernetes.Interface) {
return cs
}
func impersonationClient(user string, groups []string, scheme *runtime.Scheme) client.Client {
c, err := config.GetConfig()
Expect(err).ToNot(HaveOccurred())
c.Impersonate = rest.ImpersonationConfig{
UserName: user,
Groups: groups,
}
cl, err := client.New(c, client.Options{Scheme: scheme})
Expect(err).ToNot(HaveOccurred())
return cl
}
func withDefaultGroups(groups []string) []string {
return append([]string{"projectcapsule.dev"}, groups...)
}

View File

@@ -7,7 +7,7 @@ import (
"context"
"time"
"github.com/projectcapsule/capsule/pkg/utils"
"github.com/projectcapsule/capsule/pkg/meta"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
@@ -60,6 +60,19 @@ var _ = Describe("cordoning a Tenant", Label("tenant"), func() {
},
},
}
By("Verifing Tenant Status", func() {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
condition := t.Status.Conditions.GetConditionByType(meta.CordonedCondition)
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
Expect(condition.Status).To(Equal(metav1.ConditionFalse), "Expected tenant condition status to be True")
Expect(condition.Type).To(Equal(meta.CordonedCondition), "Expected tenant condition type to be Ready")
Expect(condition.Reason).To(Equal(meta.ActiveReason), "Expected tenant condition reason to be Succeeded")
})
By("creating a Namespace", func() {
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
@@ -79,10 +92,22 @@ var _ = Describe("cordoning a Tenant", Label("tenant"), func() {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
Expect(ns.Labels).To(HaveKey(utils.CordonedLabel))
Expect(ns.Labels).To(HaveKey(meta.CordonedLabel))
})
By("Verifing Tenant Status", func() {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
condition := t.Status.Conditions.GetConditionByType(meta.CordonedCondition)
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected tenant condition status to be True")
Expect(condition.Type).To(Equal(meta.CordonedCondition), "Expected tenant condition type to be Ready")
Expect(condition.Reason).To(Equal(meta.CordonedReason), "Expected tenant condition reason to be Succeeded")
})
By("cordoning the Tenant deletion must be blocked", func() {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.Name}, tnt)).Should(Succeed())
@@ -116,8 +141,20 @@ var _ = Describe("cordoning a Tenant", Label("tenant"), func() {
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
Expect(ns.Labels).ToNot(HaveKey(utils.CordonedLabel))
Expect(ns.Labels).ToNot(HaveKey(meta.CordonedLabel))
})
By("Verifing Tenant Status", func() {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
condition := t.Status.Conditions.GetConditionByType(meta.CordonedCondition)
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
Expect(condition.Status).To(Equal(metav1.ConditionFalse), "Expected tenant condition status to be True")
Expect(condition.Type).To(Equal(meta.CordonedCondition), "Expected tenant condition type to be Ready")
Expect(condition.Reason).To(Equal(meta.ActiveReason), "Expected tenant condition reason to be Succeeded")
})
})
})

48
go.mod
View File

@@ -2,28 +2,29 @@ module github.com/projectcapsule/capsule
go 1.24.0
toolchain go1.24.6
toolchain go1.25.3
require (
github.com/go-logr/logr v1.4.3
github.com/onsi/ginkgo/v2 v2.25.1
github.com/onsi/ginkgo/v2 v2.26.0
github.com/onsi/gomega v1.38.2
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.23.0
github.com/prometheus/client_golang v1.23.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/valyala/fasttemplate v1.2.2
go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.27.0
golang.org/x/sync v0.16.0
k8s.io/api v0.34.0
k8s.io/apiextensions-apiserver v0.34.0
k8s.io/apimachinery v0.34.0
k8s.io/client-go v0.34.0
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
sigs.k8s.io/cluster-api v1.11.1
sigs.k8s.io/controller-runtime v0.22.0
sigs.k8s.io/gateway-api v1.3.0
golang.org/x/sync v0.17.0
k8s.io/api v0.34.1
k8s.io/apiextensions-apiserver v0.34.1
k8s.io/apimachinery v0.34.1
k8s.io/apiserver v0.34.1
k8s.io/client-go v0.34.1
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
sigs.k8s.io/cluster-api v1.11.2
sigs.k8s.io/controller-runtime v0.22.3
sigs.k8s.io/gateway-api v1.4.0
)
require (
@@ -59,7 +60,6 @@ require (
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
@@ -68,29 +68,29 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.0 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/oauth2 v0.31.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.13.0 // indirect
golang.org/x/tools v0.36.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250902184714-7fc278399c7f // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect

72
go.sum
View File

@@ -98,8 +98,6 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
@@ -134,8 +132,10 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY=
github.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk=
github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw=
github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE=
github.com/onsi/ginkgo/v2 v2.26.0 h1:1J4Wut1IlYZNEAWIV3ALrT9NfiaGW2cDCJQSFQMs/gE=
github.com/onsi/ginkgo/v2 v2.26.0/go.mod h1:qhEywmzWTBUY88kfO0BRvX4py7scov9yR+Az2oavUzw=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -147,14 +147,12 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/common v0.66.0 h1:K/rJPHrG3+AoQs50r2+0t7zMnMzek2Vbv31OFVsMeVY=
github.com/prometheus/common v0.66.0/go.mod h1:Ux6NtV1B4LatamKE63tJBntoxD++xmtI/lK0VtEplN4=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
@@ -216,6 +214,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -223,36 +223,53 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -273,6 +290,8 @@ google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -280,43 +299,60 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE=
k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug=
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc=
k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0=
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0=
k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.0 h1:Z51fw1iGMqN7uJ1kEaynf2Aec1Y774PqU+FVWCFV3Jg=
k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ=
k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo=
k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY=
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
k8s.io/cluster-bootstrap v0.33.3 h1:u2NTxJ5CFSBFXaDxLQoOWMly8eni31psVso+caq6uwI=
k8s.io/cluster-bootstrap v0.33.3/go.mod h1:p970f8u8jf273zyQ5raD8WUu2XyAl0SAWOY82o7i/ds=
k8s.io/component-base v0.34.0 h1:bS8Ua3zlJzapklsB1dZgjEJuJEeHjj8yTu1gxE2zQX8=
k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg=
k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 h1:liMHz39T5dJO1aOKHLvwaCjDbf07wVh6yaUlTpunnkE=
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/kube-openapi v0.0.0-20250902184714-7fc278399c7f h1:wyRlmLgBSXi3kgawro8klrMRljXeRo1HFkQRs+meYfs=
k8s.io/kube-openapi v0.0.0-20250902184714-7fc278399c7f/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0=
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/cluster-api v1.11.0 h1:4ZqKxjhdP3F/vvHMd675rGsDrT/siggnFPt5eKQ8nkI=
sigs.k8s.io/cluster-api v1.11.0/go.mod h1:gGmNlHrtJe3z0YV3J6JRy5Rwh9SfzokjQaS+Fv3DBPE=
sigs.k8s.io/cluster-api v1.11.1 h1:7CyGCTxv1p3Y2kRe1ljTj/w4TcdIdWNj0CTBc4i1aBo=
sigs.k8s.io/cluster-api v1.11.1/go.mod h1:zyrjgJ5RbXhwKcAdUlGPNK5YOHpcmxXvur+5I8lkMUQ=
sigs.k8s.io/controller-runtime v0.22.0 h1:mTOfibb8Hxwpx3xEkR56i7xSjB+nH4hZG37SrlCY5e0=
sigs.k8s.io/controller-runtime v0.22.0/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY=
sigs.k8s.io/cluster-api v1.11.2 h1:uAczaBavU5Y6aDgyoXWtq28k1kalpSZnVItwXHusw1c=
sigs.k8s.io/cluster-api v1.11.2/go.mod h1:C1gJVAjMXRG+M+djjGYNkoi5kBMhFnOUI9QqZDAtMms=
sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg=
sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY=
sigs.k8s.io/controller-runtime v0.22.2 h1:cK2l8BGWsSWkXz09tcS4rJh95iOLney5eawcK5A33r4=
sigs.k8s.io/controller-runtime v0.22.2/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y=
sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
sigs.k8s.io/gateway-api v1.3.0 h1:q6okN+/UKDATola4JY7zXzx40WO4VISk7i9DIfOvr9M=
sigs.k8s.io/gateway-api v1.3.0/go.mod h1:d8NV8nJbaRbEKem+5IuxkL8gJGOZ+FJ+NvOIltV8gDk=
sigs.k8s.io/gateway-api v1.4.0 h1:ZwlNM6zOHq0h3WUX2gfByPs2yAEsy/EenYJB78jpQfQ=
sigs.k8s.io/gateway-api v1.4.0/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=

View File

@@ -16,6 +16,7 @@ type AdditionalMetadataSpec struct {
type AdditionalMetadataSelectorSpec struct {
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
}

View File

@@ -11,4 +11,8 @@ type AdditionalRoleBindingsSpec struct {
ClusterRoleName string `json:"clusterRoleName"`
// kubebuilder:validation:Minimum=1
Subjects []rbacv1.Subject `json:"subjects"`
// Additional Labels for the synchronized rolebindings
Labels map[string]string `json:"labels,omitempty"`
// Additional Annotations for the synchronized rolebindings
Annotations map[string]string `json:"annotations,omitempty"`
}

View File

@@ -79,7 +79,7 @@ func (in *AllowedListSpec) ExactMatch(value string) (ok bool) {
ok = i < len(in.Exact) && in.Exact[i] == value
}
return
return ok
}
func (in *AllowedListSpec) RegexMatch(value string) (ok bool) {
@@ -87,7 +87,7 @@ func (in *AllowedListSpec) RegexMatch(value string) (ok bool) {
ok = regexp.MustCompile(in.Regex).MatchString(value)
}
return
return ok
}
// +kubebuilder:object:generate=true

View File

@@ -35,7 +35,7 @@ func (in ForbiddenListSpec) ExactMatch(value string) (ok bool) {
ok = i < len(in.Exact) && in.Exact[i] == value
}
return
return ok
}
func (in ForbiddenListSpec) RegexMatch(value string) (ok bool) {
@@ -43,7 +43,7 @@ func (in ForbiddenListSpec) RegexMatch(value string) (ok bool) {
ok = regexp.MustCompile(in.Regex).MatchString(value)
}
return
return ok
}
type ForbiddenError struct {
@@ -76,7 +76,7 @@ func (f *ForbiddenError) appendForbiddenError() (append string) {
append += fmt.Sprintf("matching the regex %s", f.spec.Regex)
}
return
return append
}
func ValidateForbidden(metadata map[string]string, forbiddenList ForbiddenListSpec) error {

View File

@@ -85,6 +85,20 @@ func (in *AdditionalRoleBindingsSpec) DeepCopyInto(out *AdditionalRoleBindingsSp
*out = make([]rbacv1.Subject, len(*in))
copy(*out, *in)
}
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalRoleBindingsSpec.

View File

@@ -46,7 +46,7 @@ func (c CapsuleCA) CACertificatePem() (b *bytes.Buffer, err error) {
crtBytes, err = x509.CreateCertificate(rand.Reader, c.certificate, c.certificate, &c.key.PublicKey, c.key)
if err != nil {
return
return b, err
}
b = new(bytes.Buffer)
@@ -111,7 +111,7 @@ func GenerateCertificateAuthority() (s *CapsuleCA, err error) {
return nil, err
}
return
return s, err
}
func GetCertificateFromBytes(certBytes []byte) (*x509.Certificate, error) {
@@ -144,7 +144,6 @@ func GetCertificateWithPrivateKeyFromBytes(certBytes, keyBytes []byte) (*x509.Ce
return cert, key, nil
}
//nolint:nakedret
func (c *CapsuleCA) GenerateCertificate(opts CertificateOptions) (certificatePem *bytes.Buffer, certificateKey *bytes.Buffer, err error) {
var certPrivKey *rsa.PrivateKey
@@ -185,7 +184,7 @@ func (c *CapsuleCA) GenerateCertificate(opts CertificateOptions) (certificatePem
Bytes: certBytes,
})
if err != nil {
return
return certificatePem, certificateKey, err
}
certificateKey = new(bytes.Buffer)
@@ -195,8 +194,8 @@ func (c *CapsuleCA) GenerateCertificate(opts CertificateOptions) (certificatePem
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
})
if err != nil {
return
return certificatePem, certificateKey, err
}
return
return certificatePem, certificateKey, err
}

View File

@@ -36,6 +36,7 @@ func NewCapsuleConfiguration(ctx context.Context, client client.Client, name str
},
}
}
panic(errors.Wrap(err, "Cannot retrieve Capsule configuration with name "+name))
}
@@ -69,6 +70,10 @@ func (c *capsuleConfiguration) EnableTLSConfiguration() bool {
return c.retrievalFn().Spec.EnableTLSReconciler
}
func (c *capsuleConfiguration) AllowServiceAccountPromotion() bool {
return c.retrievalFn().Spec.AllowServiceAccountPromotion
}
func (c *capsuleConfiguration) MutatingWebhookConfigurationName() (name string) {
return c.retrievalFn().Spec.CapsuleResources.MutatingWebhookConfigurationName
}

View File

@@ -19,6 +19,7 @@ type Configuration interface {
// EnableTLSConfiguration enabled the TLS reconciler, responsible for creating CA and TLS certificate required
// for the CRD conversion and webhooks.
EnableTLSConfiguration() bool
AllowServiceAccountPromotion() bool
TLSSecretName() string
MutatingWebhookConfigurationName() string
ValidatingWebhookConfigurationName() string

View File

@@ -50,6 +50,6 @@ func (s HostnamePath) Func() client.IndexerFunc {
}
}
return
return entries
}
}

View File

@@ -11,6 +11,7 @@ import (
const (
// ReadyCondition indicates the resource is ready and fully reconciled.
ReadyCondition string = "Ready"
CordonedCondition string = "Cordoned"
NotReadyCondition string = "NotReady"
AssignedCondition string = "Assigned"
@@ -19,11 +20,105 @@ const (
// FailedReason indicates a condition or event observed a failure (Claim Rejected).
SucceededReason string = "Succeeded"
FailedReason string = "Failed"
ActiveReason string = "Active"
CordonedReason string = "Cordoned"
PoolExhaustedReason string = "PoolExhausted"
QueueExhaustedReason string = "QueueExhausted"
NamespaceExhaustedReason string = "NamespaceExhausted"
)
// +kubebuilder:object:generate=true
type ConditionList []Condition
// Adds a condition by type.
func (c *ConditionList) GetConditionByType(conditionType string) *Condition {
for i := range *c {
if (*c)[i].Type == conditionType {
return &(*c)[i]
}
}
return nil
}
// Adds a condition by type.
func (c *ConditionList) UpdateConditionByType(condition Condition) {
for i, cond := range *c {
if cond.Type == condition.Type {
(*c)[i].UpdateCondition(condition)
return
}
}
*c = append(*c, condition)
}
// Removes a condition by type.
func (c *ConditionList) RemoveConditionByType(condition Condition) {
if c == nil {
return
}
filtered := make(ConditionList, 0, len(*c))
for _, cond := range *c {
if cond.Type != condition.Type {
filtered = append(filtered, cond)
}
}
*c = filtered
}
// +kubebuilder:object:generate=true
type Condition metav1.Condition
func NewReadyCondition(obj client.Object) Condition {
return Condition{
Type: ReadyCondition,
Status: metav1.ConditionTrue,
Reason: SucceededReason,
Message: "reconciled",
LastTransitionTime: metav1.Now(),
}
}
func NewCordonedCondition(obj client.Object) Condition {
return Condition{
Type: CordonedCondition,
Status: metav1.ConditionFalse,
Reason: ActiveReason,
Message: "not cordoned",
LastTransitionTime: metav1.Now(),
}
}
// Disregards fields like LastTransitionTime and Version, which are not relevant for the API.
func (c *Condition) UpdateCondition(condition Condition) (updated bool) {
if condition.Type == c.Type &&
condition.Status == c.Status &&
condition.Reason == c.Reason &&
condition.Message == c.Message &&
condition.ObservedGeneration == c.ObservedGeneration {
return false
}
if condition.Status != c.Status {
c.LastTransitionTime = metav1.Now()
}
c.Type = condition.Type
c.Status = condition.Status
c.Reason = condition.Reason
c.Message = condition.Message
c.ObservedGeneration = condition.ObservedGeneration
c.LastTransitionTime = condition.LastTransitionTime
return true
}
func NewBoundCondition(obj client.Object) metav1.Condition {
return metav1.Condition{
Type: BoundCondition,

211
pkg/meta/conditions_test.go Normal file
View File

@@ -0,0 +1,211 @@
// Copyright 2020-2025 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package meta
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// helper
func makeCond(tpe, status, reason, msg string, gen int64) Condition {
return Condition{
Type: tpe,
Status: metav1.ConditionStatus(status),
Reason: reason,
Message: msg,
ObservedGeneration: gen,
LastTransitionTime: metav1.NewTime(time.Unix(0, 0)),
}
}
func TestConditionList_GetConditionByType(t *testing.T) {
t.Run("returns matching condition", func(t *testing.T) {
list := ConditionList{
makeCond("Ready", "False", "Init", "starting", 1),
makeCond("Synced", "True", "Ok", "done", 2),
}
got := list.GetConditionByType("Synced")
assert.NotNil(t, got)
assert.Equal(t, "Synced", got.Type)
assert.Equal(t, metav1.ConditionTrue, got.Status)
assert.Equal(t, "Ok", got.Reason)
assert.Equal(t, "done", got.Message)
})
t.Run("returns nil when not found", func(t *testing.T) {
list := ConditionList{
makeCond("Ready", "False", "Init", "starting", 1),
}
assert.Nil(t, list.GetConditionByType("Missing"))
})
t.Run("returned pointer refers to slice element (not copy)", func(t *testing.T) {
list := ConditionList{
makeCond("Ready", "False", "Init", "starting", 1),
makeCond("Synced", "True", "Ok", "done", 2),
}
ptr := list.GetConditionByType("Ready")
assert.NotNil(t, ptr)
ptr.Message = "mutated"
// This asserts GetConditionByType returns &list[i] (via index),
// not &cond where cond is the range variable copy.
assert.Equal(t, "mutated", list[0].Message)
})
}
func TestConditionList_UpdateConditionByType(t *testing.T) {
now := metav1.Now()
t.Run("updates existing condition in place", func(t *testing.T) {
list := ConditionList{
makeCond("Ready", "False", "Init", "starting", 1),
makeCond("Synced", "True", "Ok", "done", 2),
}
beforeLen := len(list)
list.UpdateConditionByType(Condition{
Type: "Ready",
Status: metav1.ConditionTrue,
Reason: "Reconciled",
Message: "ready now",
ObservedGeneration: 3,
LastTransitionTime: now,
})
assert.Equal(t, beforeLen, len(list))
got := list.GetConditionByType("Ready")
assert.NotNil(t, got)
assert.Equal(t, metav1.ConditionTrue, got.Status)
assert.Equal(t, "Reconciled", got.Reason)
assert.Equal(t, "ready now", got.Message)
assert.Equal(t, int64(3), got.ObservedGeneration)
})
t.Run("appends when condition type not present", func(t *testing.T) {
list := ConditionList{
makeCond("Ready", "True", "Ok", "ready", 1),
}
beforeLen := len(list)
list.UpdateConditionByType(Condition{
Type: "Synced",
Status: metav1.ConditionTrue,
Reason: "Done",
Message: "synced",
ObservedGeneration: 2,
LastTransitionTime: now,
})
assert.Equal(t, beforeLen+1, len(list))
got := list.GetConditionByType("Synced")
assert.NotNil(t, got)
assert.Equal(t, metav1.ConditionTrue, got.Status)
assert.Equal(t, "Done", got.Reason)
assert.Equal(t, "synced", got.Message)
assert.Equal(t, int64(2), got.ObservedGeneration)
})
}
func TestConditionList_RemoveConditionByType(t *testing.T) {
t.Run("removes all conditions with matching type", func(t *testing.T) {
list := ConditionList{
makeCond("A", "True", "x", "m1", 1),
makeCond("B", "True", "y", "m2", 1),
makeCond("A", "False", "z", "m3", 2),
}
list.RemoveConditionByType(Condition{Type: "A"})
assert.Len(t, list, 1)
assert.Equal(t, "B", list[0].Type)
})
t.Run("no-op when type not present", func(t *testing.T) {
orig := ConditionList{
makeCond("A", "True", "x", "m1", 1),
}
list := append(ConditionList{}, orig...) // copy
list.RemoveConditionByType(Condition{Type: "Missing"})
assert.Equal(t, orig, list)
})
t.Run("nil receiver is safe", func(t *testing.T) {
var list *ConditionList // nil receiver
assert.NotPanics(t, func() {
list.RemoveConditionByType(Condition{Type: "X"})
})
})
}
func TestUpdateCondition(t *testing.T) {
now := metav1.Now()
t.Run("no update when all relevant fields match", func(t *testing.T) {
c := &Condition{
Type: "Ready",
Status: "True",
Reason: "Success",
Message: "All good",
}
updated := c.UpdateCondition(Condition{
Type: "Ready",
Status: "True",
Reason: "Success",
Message: "All good",
LastTransitionTime: now,
})
assert.False(t, updated)
})
t.Run("update occurs on message change", func(t *testing.T) {
c := &Condition{
Type: "Ready",
Status: "True",
Reason: "Success",
Message: "Old message",
}
updated := c.UpdateCondition(Condition{
Type: "Ready",
Status: "True",
Reason: "Success",
Message: "New message",
LastTransitionTime: now,
})
assert.True(t, updated)
assert.Equal(t, "New message", c.Message)
})
t.Run("update occurs on status change", func(t *testing.T) {
c := &Condition{
Type: "Ready",
Status: "False",
Reason: "Pending",
Message: "Not ready yet",
}
updated := c.UpdateCondition(Condition{
Type: "Ready",
Status: "True",
Reason: "Success",
Message: "Ready",
LastTransitionTime: now,
})
assert.True(t, updated)
assert.Equal(t, "True", string(c.Status))
assert.Equal(t, "Success", c.Reason)
assert.Equal(t, "Ready", c.Message)
})
}

View File

@@ -12,6 +12,14 @@ import (
const (
FreezeLabel = "projectcapsule.dev/freeze"
FreezeLabelTrigger = "true"
OwnerPromotionLabel = "owner.projectcapsule.dev/promote"
OwnerPromotionLabelTrigger = "true"
CordonedLabel = "projectcapsule.dev/cordoned"
CordonedLabelTrigger = "true"
ManagedByCapsuleLabel = "capsule.clastix.io/managed-by"
)
func FreezeLabelTriggers(obj client.Object) bool {
@@ -22,6 +30,14 @@ func FreezeLabelRemove(obj client.Object) {
labelRemove(obj, FreezeLabel)
}
func OwnerPromotionLabelTriggers(obj client.Object) bool {
return labelTriggers(obj, OwnerPromotionLabel, OwnerPromotionLabelTrigger)
}
func OwnerPromotionLabelRemove(obj client.Object) {
labelRemove(obj, OwnerPromotionLabel)
}
func labelRemove(obj client.Object, anno string) {
annotations := obj.GetLabels()

61
pkg/meta/labels_test.go Normal file
View File

@@ -0,0 +1,61 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package meta
import (
"testing"
corev1 "k8s.io/api/core/v1"
)
func TestFreezeLabel(t *testing.T) {
ns := &corev1.Namespace{}
ns.SetLabels(map[string]string{})
// absent
if FreezeLabelTriggers(ns) {
t.Errorf("expected FreezeLabelTriggers to be false when label is absent")
}
// set to trigger
ns.Labels[FreezeLabel] = FreezeLabelTrigger
if !FreezeLabelTriggers(ns) {
t.Errorf("expected FreezeLabelTriggers to be true when label is set to trigger")
}
ns.Labels[FreezeLabel] = "false"
if FreezeLabelTriggers(ns) {
t.Errorf("expected FreezeLabelTriggers to be false when label is not set to trigger")
}
// remove
FreezeLabelRemove(ns)
if _, ok := ns.Labels[FreezeLabel]; ok {
t.Errorf("expected FreezeLabel to be removed")
}
}
func TestOwnerPromotionLabel(t *testing.T) {
ns := &corev1.Namespace{}
ns.SetLabels(map[string]string{})
if OwnerPromotionLabelTriggers(ns) {
t.Errorf("expected OwnerPromotionLabelTriggers to be false when label is absent")
}
ns.Labels[OwnerPromotionLabel] = OwnerPromotionLabelTrigger
if !OwnerPromotionLabelTriggers(ns) {
t.Errorf("expected OwnerPromotionLabelTriggers to be true when label is set to trigger")
}
ns.Labels[OwnerPromotionLabel] = "false"
if OwnerPromotionLabelTriggers(ns) {
t.Errorf("expected OwnerPromotionLabelTriggers to be false when label is not set to trigger")
}
OwnerPromotionLabelRemove(ns)
if _, ok := ns.Labels[OwnerPromotionLabel]; ok {
t.Errorf("expected OwnerPromotionLabel to be removed")
}
}

View File

@@ -0,0 +1,47 @@
//go:build !ignore_autogenerated
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
// Code generated by controller-gen. DO NOT EDIT.
package meta
import ()
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Condition) DeepCopyInto(out *Condition) {
*out = *in
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition.
func (in *Condition) DeepCopy() *Condition {
if in == nil {
return nil
}
out := new(Condition)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in ConditionList) DeepCopyInto(out *ConditionList) {
{
in := &in
*out = make(ConditionList, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConditionList.
func (in ConditionList) DeepCopy() ConditionList {
if in == nil {
return nil
}
out := new(ConditionList)
in.DeepCopyInto(out)
return *out
}

View File

@@ -10,7 +10,8 @@ import (
type TenantRecorder struct {
TenantNamespaceRelationshipGauge *prometheus.GaugeVec
TenantCordonedStatusGauge *prometheus.GaugeVec
TenantNamespaceConditionGauge *prometheus.GaugeVec
TenantConditionGauge *prometheus.GaugeVec
TenantNamespaceCounterGauge *prometheus.GaugeVec
TenantResourceUsageGauge *prometheus.GaugeVec
TenantResourceLimitGauge *prometheus.GaugeVec
@@ -30,14 +31,22 @@ func NewTenantRecorder() *TenantRecorder {
Namespace: metricsPrefix,
Name: "tenant_namespace_relationship",
Help: "Mapping metric showing namespace to tenant relationships",
}, []string{"tenant", "namespace"},
}, []string{"tenant", "target_namespace"},
),
TenantCordonedStatusGauge: prometheus.NewGaugeVec(
TenantNamespaceConditionGauge: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: metricsPrefix,
Name: "tenant_status",
Help: "Tenant cordon state indicating if tenant operations are restricted (1) or allowed (0) for resource creation and modification",
}, []string{"tenant"},
Name: "tenant_namespace_condition",
Help: "Provides per namespace within a tenant condition status for each condition",
}, []string{"tenant", "target_namespace", "condition"},
),
TenantConditionGauge: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: metricsPrefix,
Name: "tenant_condition",
Help: "Provides per tenant condition status for each condition",
}, []string{"tenant", "condition"},
),
TenantNamespaceCounterGauge: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
@@ -66,13 +75,59 @@ func NewTenantRecorder() *TenantRecorder {
func (r *TenantRecorder) Collectors() []prometheus.Collector {
return []prometheus.Collector{
r.TenantNamespaceRelationshipGauge,
r.TenantCordonedStatusGauge,
r.TenantNamespaceConditionGauge,
r.TenantConditionGauge,
r.TenantNamespaceCounterGauge,
r.TenantResourceUsageGauge,
r.TenantResourceLimitGauge,
}
}
func (r *TenantRecorder) DeleteAllMetricsForNamespace(namespace string) {
r.DeleteNamespaceRelationshipMetrics(namespace)
r.DeleteTenantNamespaceConditionMetrics(namespace)
}
// DeleteCondition deletes the condition metrics for the ref.
func (r *TenantRecorder) DeleteNamespaceRelationshipMetrics(namespace string) {
r.TenantNamespaceRelationshipGauge.DeletePartialMatch(map[string]string{
"target_namespace": namespace,
})
}
func (r *TenantRecorder) DeleteTenantNamespaceConditionMetrics(namespace string) {
r.TenantNamespaceConditionGauge.DeletePartialMatch(map[string]string{
"target_namespace": namespace,
})
}
func (r *TenantRecorder) DeleteTenantNamespaceConditionMetricByType(namespace string, condition string) {
r.TenantNamespaceConditionGauge.DeletePartialMatch(map[string]string{
"target_namespace": namespace,
"condition": condition,
})
}
func (r *TenantRecorder) DeleteAllMetricsForTenant(tenant string) {
r.DeleteTenantResourceMetrics(tenant)
r.DeleteTenantStatusMetrics(tenant)
r.DeleteTenantConditionMetrics(tenant)
r.DeleteTenantResourceMetrics(tenant)
}
func (r *TenantRecorder) DeleteTenantConditionMetrics(tenant string) {
r.TenantConditionGauge.DeletePartialMatch(map[string]string{
"tenant": tenant,
})
}
func (r *TenantRecorder) DeleteTenantConditionMetricByType(tenant string, condition string) {
r.TenantConditionGauge.DeletePartialMatch(map[string]string{
"tenant": tenant,
"condition": condition,
})
}
// DeleteCondition deletes the condition metrics for the ref.
func (r *TenantRecorder) DeleteTenantResourceMetrics(tenant string) {
r.TenantResourceUsageGauge.DeletePartialMatch(map[string]string{
@@ -85,25 +140,13 @@ func (r *TenantRecorder) DeleteTenantResourceMetrics(tenant string) {
// DeleteCondition deletes the condition metrics for the ref.
func (r *TenantRecorder) DeleteTenantStatusMetrics(tenant string) {
r.TenantNamespaceCounterGauge.DeletePartialMatch(map[string]string{
"tenant": tenant,
})
r.TenantNamespaceRelationshipGauge.DeletePartialMatch(map[string]string{
"tenant": tenant,
})
r.TenantResourceUsageGauge.DeletePartialMatch(map[string]string{
"tenant": tenant,
})
r.TenantResourceLimitGauge.DeletePartialMatch(map[string]string{
r.TenantNamespaceConditionGauge.DeletePartialMatch(map[string]string{
"tenant": tenant,
})
}
// DeleteCondition deletes the condition metrics for the ref.
func (r *TenantRecorder) DeleteNamespaceRelationshipMetrics(namespace string) {
r.TenantNamespaceRelationshipGauge.DeletePartialMatch(map[string]string{
"namespace": namespace,
})
}
func (r *TenantRecorder) DeleteAllMetrics(tenant string) {
r.DeleteTenantResourceMetrics(tenant)
r.DeleteTenantStatusMetrics(tenant)
}

16
pkg/utils/maps.go Normal file
View File

@@ -0,0 +1,16 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package utils
func MapMergeNoOverrite(dst, src map[string]string) {
if len(src) == 0 {
return
}
for k, v := range src {
if _, exists := dst[k]; !exists {
dst[k] = v
}
}
}

98
pkg/utils/maps_test.go Normal file
View File

@@ -0,0 +1,98 @@
package utils
import (
"reflect"
"testing"
)
func TestMapMergeNoOverrite_AddsNonOverlapping(t *testing.T) {
dst := map[string]string{"a": "1"}
src := map[string]string{"b": "2"}
MapMergeNoOverrite(dst, src)
if got, want := dst["a"], "1"; got != want {
t.Fatalf("dst[a] = %q, want %q", got, want)
}
if got, want := dst["b"], "2"; got != want {
t.Fatalf("dst[b] = %q, want %q", got, want)
}
if len(dst) != 2 {
t.Fatalf("len(dst) = %d, want 2", len(dst))
}
}
func TestMapMergeNoOverrite_DoesNotOverwriteExisting(t *testing.T) {
dst := map[string]string{"a": "1"}
src := map[string]string{"a": "X"} // overlapping key
MapMergeNoOverrite(dst, src)
if got, want := dst["a"], "1"; got != want {
t.Fatalf("dst[a] overwritten: got %q, want %q", got, want)
}
}
func TestMapMergeNoOverrite_EmptySrc_NoChange(t *testing.T) {
dst := map[string]string{"a": "1"}
src := map[string]string{} // empty
before := make(map[string]string, len(dst))
for k, v := range dst {
before[k] = v
}
MapMergeNoOverrite(dst, src)
if !reflect.DeepEqual(dst, before) {
t.Fatalf("dst changed with empty src: got %#v, want %#v", dst, before)
}
}
func TestMapMergeNoOverrite_NilSrc_NoChange(t *testing.T) {
dst := map[string]string{"a": "1"}
var src map[string]string // nil
before := make(map[string]string, len(dst))
for k, v := range dst {
before[k] = v
}
MapMergeNoOverrite(dst, src)
if !reflect.DeepEqual(dst, before) {
t.Fatalf("dst changed with nil src: got %#v, want %#v", dst, before)
}
}
func TestMapMergeNoOverrite_Idempotent(t *testing.T) {
dst := map[string]string{"a": "1"}
src := map[string]string{"b": "2"}
MapMergeNoOverrite(dst, src)
first := map[string]string{}
for k, v := range dst {
first[k] = v
}
// Call again; result should be identical
MapMergeNoOverrite(dst, src)
if !reflect.DeepEqual(dst, first) {
t.Fatalf("non-idempotent merge: after second merge got %#v, want %#v", dst, first)
}
}
func TestMapMergeNoOverrite_NilDst_Panics(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("expected panic when dst is nil, but did not panic")
}
}()
var dst map[string]string // nil destination map
src := map[string]string{"a": "1"}
// Writing to a nil map panics; document current behavior via this test.
MapMergeNoOverrite(dst, src)
}

View File

@@ -14,5 +14,5 @@ func GetOwnersWithKinds(tenant *capsulev1beta2.Tenant) (owners []string) {
owners = append(owners, fmt.Sprintf("%s:%s", owner.Kind.String(), owner.Name))
}
return
return owners
}

View File

@@ -15,10 +15,6 @@ import (
"github.com/projectcapsule/capsule/api/v1beta2"
)
const (
CordonedLabel = "projectcapsule.dev/cordoned"
)
func GetTypeLabel(t runtime.Object) (label string, err error) {
switch v := t.(type) {
case *v1beta1.Tenant, *v1beta2.Tenant:
@@ -37,5 +33,5 @@ func GetTypeLabel(t runtime.Object) (label string, err error) {
err = fmt.Errorf("type %T is not mapped as Capsule label recognized", v)
}
return
return label, err
}

View File

@@ -30,5 +30,5 @@ func (u userGroupList) Find(needle string) (found bool) {
found = i < len(u) && u[i] == needle
return
return found
}

View File

@@ -112,5 +112,5 @@ func appendHostnameError(spec api.AllowedListSpec) (append string) {
append += fmt.Sprintf(", or matching the regex %s", spec.Regex)
}
return
return append
}

View File

@@ -43,7 +43,7 @@ func (n NetworkingV1) IngressClass() (res *string) {
}
}
return
return res
}
func (n NetworkingV1) SetIngressClass(ingressClassName string) {
@@ -113,7 +113,7 @@ func (n NetworkingV1Beta1) IngressClass() (res *string) {
}
}
return
return res
}
func (n NetworkingV1Beta1) SetIngressClass(ingressClassName string) {
@@ -187,7 +187,7 @@ func (e Extension) IngressClass() (res *string) {
}
}
return
return res
}
func (e Extension) SetIngressClass(ingressClassName string) {
@@ -252,5 +252,5 @@ func (h HostnamesList) IsStringInList(value string) (ok bool) {
i := sort.SearchStrings(h, value)
ok = i < h.Len() && h[i] == value
return
return ok
}

View File

@@ -38,7 +38,7 @@ func FromRequest(req admission.Request, decoder admission.Decoder) (ingress Ingr
if req.Kind.Version == "v1" {
ingressObj := &networkingv1.Ingress{}
if err = decoder.Decode(req, ingressObj); err != nil {
return
return ingress, err
}
ingress = NetworkingV1{Ingress: ingressObj}
@@ -48,14 +48,14 @@ func FromRequest(req admission.Request, decoder admission.Decoder) (ingress Ingr
ingressObj := &networkingv1beta1.Ingress{}
if err = decoder.Decode(req, ingressObj); err != nil {
return
return ingress, err
}
ingress = NetworkingV1Beta1{Ingress: ingressObj}
case "extensions":
ingressObj := &extensionsv1beta1.Ingress{}
if err = decoder.Decode(req, ingressObj); err != nil {
return
return ingress, err
}
ingress = Extension{Ingress: ingressObj}
@@ -63,5 +63,5 @@ func FromRequest(req admission.Request, decoder admission.Decoder) (ingress Ingr
err = fmt.Errorf("cannot recognize type %s", req.Kind.Group)
}
return
return ingress, err
}

View File

@@ -9,6 +9,7 @@ import (
"net/http"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -16,6 +17,7 @@ import (
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/configuration"
"github.com/projectcapsule/capsule/pkg/meta"
capsuleutils "github.com/projectcapsule/capsule/pkg/utils"
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
"github.com/projectcapsule/capsule/pkg/webhook/utils"
@@ -74,16 +76,21 @@ func (h *cordoningLabelHandler) syncNamespaceCordonLabel(ctx context.Context, c
}
}
if !tnt.Spec.Cordoned {
condition := tnt.Status.Conditions.GetConditionByType(meta.CordonedCondition)
if condition == nil {
return nil
}
if condition.Status != metav1.ConditionTrue {
return nil
}
labels := ns.GetLabels()
if _, ok := labels[capsuleutils.CordonedLabel]; ok {
if _, ok := labels[meta.CordonedLabel]; ok {
return nil
}
ns.Labels[capsuleutils.CordonedLabel] = "true"
ns.Labels[meta.CordonedLabel] = "true"
marshaled, err := json.Marshal(ns)
if err != nil {

View File

@@ -13,7 +13,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsuletenant "github.com/projectcapsule/capsule/controllers/tenant"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/configuration"
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
)
@@ -49,12 +49,29 @@ func (h *metadataHandler) OnCreate(client client.Client, decoder admission.Decod
}
// sync namespace metadata
if err := capsuletenant.SyncNamespaceMetadata(tenant, ns); err != nil {
response := admission.Errored(http.StatusInternalServerError, err)
instance := tenant.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{
Name: ns.GetName(),
UID: ns.GetUID(),
})
return &response
if len(instance.Metadata.Labels) == 0 && len(instance.Metadata.Annotations) == 0 {
return nil
}
labels := ns.GetLabels()
for k, v := range instance.Metadata.Labels {
labels[k] = v
}
ns.SetLabels(labels)
annotations := ns.GetAnnotations()
for k, v := range instance.Metadata.Annotations {
annotations[k] = v
}
ns.SetAnnotations(annotations)
marshaled, err := json.Marshal(ns)
if err != nil {
response := admission.Errored(http.StatusInternalServerError, err)

View File

@@ -60,7 +60,12 @@ func (h *ownerReferenceHandler) OnUpdate(c client.Client, decoder admission.Deco
return utils.ErroredResponse(err)
}
if !h.namespaceIsOwned(oldNs, tntList, req) {
ok, err := h.namespaceIsOwned(ctx, c, oldNs, tntList, req)
if err != nil {
return utils.ErroredResponse(err)
}
if !ok {
recorder.Eventf(oldNs, corev1.EventTypeWarning, "OfflimitNamespace", "Namespace %s can not be patched", oldNs.GetName())
response := admission.Denied("Denied patch request for this namespace")
@@ -109,20 +114,25 @@ func (h *ownerReferenceHandler) OnUpdate(c client.Client, decoder admission.Deco
}
}
func (h *ownerReferenceHandler) namespaceIsOwned(ns *corev1.Namespace, tenantList *capsulev1beta2.TenantList, req admission.Request) bool {
func (h *ownerReferenceHandler) namespaceIsOwned(ctx context.Context, c client.Client, ns *corev1.Namespace, tenantList *capsulev1beta2.TenantList, req admission.Request) (bool, error) {
for _, tenant := range tenantList.Items {
for _, ownerRef := range ns.OwnerReferences {
if !capsuleutils.IsTenantOwnerReference(ownerRef) {
continue
}
if ownerRef.UID == tenant.UID && utils.IsTenantOwner(tenant.Spec.Owners, req.UserInfo) {
return true
ok, err := utils.IsTenantOwner(ctx, c, &tenant, req.UserInfo, h.cfg.AllowServiceAccountPromotion())
if err != nil {
return false, err
}
if ownerRef.UID == tenant.UID && ok {
return true, nil
}
}
}
return false
return false, nil
}
func (h *ownerReferenceHandler) setOwnerRef(ctx context.Context, req admission.Request, client client.Client, decoder admission.Decoder, recorder record.EventRecorder) *admission.Response {

View File

@@ -12,6 +12,8 @@ import (
v1 "k8s.io/api/authentication/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -19,6 +21,7 @@ import (
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/configuration"
"github.com/projectcapsule/capsule/pkg/meta"
capsuleutils "github.com/projectcapsule/capsule/pkg/utils"
"github.com/projectcapsule/capsule/pkg/webhook/utils"
)
@@ -46,7 +49,7 @@ func getNamespaceTenant(
cfg configuration.Configuration,
recorder record.EventRecorder,
) (*capsulev1beta2.Tenant, *admission.Response) {
tenant, errResponse := getTenantByLabels(ctx, client, ns, req, recorder)
tenant, errResponse := getTenantByLabels(ctx, client, ns, req, cfg, recorder)
if errResponse != nil {
return nil, errResponse
}
@@ -67,6 +70,7 @@ func getTenantByLabels(
client client.Client,
ns *corev1.Namespace,
req admission.Request,
cfg configuration.Configuration,
recorder record.EventRecorder,
) (*capsulev1beta2.Tenant, *admission.Response) {
ln, err := capsuleutils.GetTypeLabel(&capsulev1beta2.Tenant{})
@@ -85,8 +89,15 @@ func getTenantByLabels(
return nil, &response
}
// Tenant owner must adhere to user that asked for NS creation
if !utils.IsTenantOwner(tnt.Spec.Owners, req.UserInfo) {
ok, err := utils.IsTenantOwner(ctx, client, tnt, req.UserInfo, cfg.AllowServiceAccountPromotion())
if err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return nil, &response
}
if !ok {
recorder.Eventf(tnt, corev1.EventTypeWarning, "NonOwnedTenant", "Namespace %s cannot be assigned to the current Tenant", ns.GetName())
response := admission.Denied("Cannot assign the desired namespace to a non-owned Tenant")
@@ -102,6 +113,8 @@ func getTenantByLabels(
}
// getTenantByUserInfo returns tenant list associated with admission request userinfo.
//
//nolint:nestif
func getTenantByUserInfo(
ctx context.Context,
ns *corev1.Namespace,
@@ -141,6 +154,16 @@ func getTenantByUserInfo(
}
tenants = append(tenants, saTntList.Items...)
if cfg.AllowServiceAccountPromotion() {
if tnt, err := resolveServiceAccountActor(ctx, ns, userInfo, clt, cfg); err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return nil, &response
} else if tnt != nil {
tenants = append(tenants, *tnt)
}
}
}
// Group tenants.
@@ -195,6 +218,47 @@ func getTenantByUserInfo(
return nil, nil
}
func resolveServiceAccountActor(
ctx context.Context,
ns *corev1.Namespace,
userInfo v1.UserInfo,
clt client.Client,
cfg configuration.Configuration,
) (tnt *capsulev1beta2.Tenant, err error) {
parts := strings.Split(userInfo.Username, ":")
if len(parts) != 4 {
return tnt, err
}
namespace, saName := parts[2], parts[3]
sa := &corev1.ServiceAccount{}
if err = clt.Get(ctx, client.ObjectKey{Namespace: namespace, Name: saName}, sa); err != nil {
if apierrors.IsNotFound(err) {
return tnt, err
}
return tnt, err
}
if meta.OwnerPromotionLabelTriggers(ns) {
return tnt, err
}
tntList := &capsulev1beta2.TenantList{}
if err = clt.List(ctx, tntList, client.MatchingFieldsSelector{
Selector: fields.OneTermEqualSelector(".status.namespaces", namespace),
}); err != nil {
return tnt, err
}
if len(tntList.Items) > 0 {
tnt = &tntList.Items[0]
}
return tnt, err
}
func validateNamespacePrefix(ns *corev1.Namespace, tenant *capsulev1beta2.Tenant) bool {
// Check if ForceTenantPrefix is true
if tenant.Spec.ForceTenantPrefix != nil && *tenant.Spec.ForceTenantPrefix {

View File

@@ -15,15 +15,18 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/configuration"
capsuleutils "github.com/projectcapsule/capsule/pkg/utils"
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
"github.com/projectcapsule/capsule/pkg/webhook/utils"
)
type patchHandler struct{}
type patchHandler struct {
configuration configuration.Configuration
}
func PatchHandler() capsulewebhook.Handler {
return &patchHandler{}
func PatchHandler(configuration configuration.Configuration) capsulewebhook.Handler {
return &patchHandler{configuration: configuration}
}
func (r *patchHandler) OnCreate(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
@@ -66,7 +69,14 @@ func (r *patchHandler) OnUpdate(c client.Client, decoder admission.Decoder, reco
return &response
}
if utils.IsTenantOwner(tnt.Spec.Owners, req.UserInfo) {
ok, err := utils.IsTenantOwner(ctx, c, tnt, req.UserInfo, r.configuration.AllowServiceAccountPromotion())
if err != nil {
response := admission.Errored(http.StatusBadRequest, err)
return &response
}
if ok {
return nil
}
}

View File

@@ -73,7 +73,7 @@ func (r *handler) handle(ctx context.Context, req admission.Request, client clie
objectLabel, err := capsuleutils.GetTypeLabel(&networkingv1.NetworkPolicy{})
if err != nil {
return
return allowed, err
}
labels := np.GetLabels()
@@ -81,5 +81,5 @@ func (r *handler) handle(ctx context.Context, req admission.Request, client clie
allowed = false
}
return
return allowed, err
}

View File

@@ -24,7 +24,7 @@ func appendForbiddenError(spec *capsulev1beta2.ForbiddenListSpec) (append string
append += fmt.Sprintf("matching the regex %s", spec.Regex)
}
return
return append
}
type nodeLabelForbiddenError struct {

View File

@@ -13,14 +13,19 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/configuration"
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
"github.com/projectcapsule/capsule/pkg/webhook/utils"
)
type containerRegistryHandler struct{}
type containerRegistryHandler struct {
configuration configuration.Configuration
}
func ContainerRegistry() capsulewebhook.Handler {
return &containerRegistryHandler{}
func ContainerRegistry(configuration configuration.Configuration) capsulewebhook.Handler {
return &containerRegistryHandler{
configuration: configuration,
}
}
func (h *containerRegistryHandler) OnCreate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
@@ -42,7 +47,13 @@ func (h *containerRegistryHandler) OnUpdate(c client.Client, decoder admission.D
}
}
func (h *containerRegistryHandler) validate(ctx context.Context, c client.Client, decoder admission.Decoder, recorder record.EventRecorder, req admission.Request) *admission.Response {
func (h *containerRegistryHandler) validate(
ctx context.Context,
c client.Client,
decoder admission.Decoder,
recorder record.EventRecorder,
req admission.Request,
) *admission.Response {
pod := &corev1.Pod{}
if err := decoder.Decode(req, pod); err != nil {
return utils.ErroredResponse(err)
@@ -61,34 +72,45 @@ func (h *containerRegistryHandler) validate(ctx context.Context, c client.Client
tnt := tntList.Items[0]
if tnt.Spec.ContainerRegistries != nil {
// Evaluate init containers
for _, container := range pod.Spec.InitContainers {
if response := h.verifyContainerRegistry(recorder, req, container, tnt); response != nil {
return response
}
}
if tnt.Spec.ContainerRegistries == nil {
return nil
}
// Evaluate containers
for _, container := range pod.Spec.Containers {
if response := h.verifyContainerRegistry(recorder, req, container, tnt); response != nil {
return response
}
for _, container := range pod.Spec.InitContainers {
if response := h.verifyContainerRegistry(recorder, req, container.Image, tnt); response != nil {
return response
}
}
for _, container := range pod.Spec.EphemeralContainers {
if response := h.verifyContainerRegistry(recorder, req, container.Image, tnt); response != nil {
return response
}
}
for _, container := range pod.Spec.Containers {
if response := h.verifyContainerRegistry(recorder, req, container.Image, tnt); response != nil {
return response
}
}
return nil
}
func (h *containerRegistryHandler) verifyContainerRegistry(recorder record.EventRecorder, req admission.Request, container corev1.Container, tnt capsulev1beta2.Tenant) *admission.Response {
func (h *containerRegistryHandler) verifyContainerRegistry(
recorder record.EventRecorder,
req admission.Request,
image string,
tnt capsulev1beta2.Tenant,
) *admission.Response {
var valid, matched bool
reg := NewRegistry(container.Image)
reg := NewRegistry(image, h.configuration)
if len(reg.Registry()) == 0 {
recorder.Eventf(&tnt, corev1.EventTypeWarning, "MissingFQCI", "Pod %s/%s is not using a fully qualified container image, cannot enforce registry the current Tenant", req.Namespace, req.Name, reg.Registry())
response := admission.Denied(NewContainerRegistryForbidden(container.Image, *tnt.Spec.ContainerRegistries).Error())
response := admission.Denied(NewContainerRegistryForbidden(image, *tnt.Spec.ContainerRegistries).Error())
return &response
}
@@ -100,7 +122,7 @@ func (h *containerRegistryHandler) verifyContainerRegistry(recorder record.Event
if !valid && !matched {
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenContainerRegistry", "Pod %s/%s is using a container hosted on registry %s that is forbidden for the current Tenant", req.Namespace, req.Name, reg.Registry())
response := admission.Denied(NewContainerRegistryForbidden(container.Image, *tnt.Spec.ContainerRegistries).Error())
response := admission.Denied(NewContainerRegistryForbidden(reg.FQCI(), *tnt.Spec.ContainerRegistries).Error())
return &response
}

View File

@@ -49,5 +49,5 @@ func (f registryClassForbiddenError) Error() (err string) {
err += strings.Join(extra, " or ")
return
return err
}

View File

@@ -4,7 +4,11 @@
package pod
import (
"fmt"
"regexp"
"strings"
"github.com/projectcapsule/capsule/pkg/configuration"
)
type registry map[string]string
@@ -49,14 +53,46 @@ func (r registry) Tag() string {
return res
}
func (r registry) FQCI() string {
reg := r.Registry()
repo := r.Repository()
img := r.Image()
tag := r.Tag()
// If there's no image, nothing to return
if img == "" {
return ""
}
// ensure repo ends with "/" if set
if repo != "" && repo[len(repo)-1] != '/' {
repo += "/"
}
// always append tag to image (strip any trailing : from image just in case)
// but our Image() already includes the name:tag, so split carefully
name := img
if tag != "" && !strings.Contains(img, ":") {
name = fmt.Sprintf("%s:%s", img, tag)
}
// build: [registry/]repo+image
if reg != "" {
return fmt.Sprintf("%s/%s%s", reg, repo, name)
}
return fmt.Sprintf("%s%s", repo, name)
}
type Registry interface {
Registry() string
Repository() string
Image() string
Tag() string
FQCI() string
}
func NewRegistry(value string) Registry {
func NewRegistry(value string, cfg configuration.Configuration) Registry {
reg := make(registry)
r := regexp.MustCompile(`((?P<registry>[a-zA-Z0-9-._]+(:\d+)?)\/)?(?P<repository>.*\/)?(?P<image>[a-zA-Z0-9-._]+:(?P<tag>[a-zA-Z0-9-._]+))?`)
match := r.FindStringSubmatch(value)

View File

@@ -25,49 +25,13 @@ func ImagePullPolicy() capsulewebhook.Handler {
func (r *imagePullPolicy) OnCreate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
pod := &corev1.Pod{}
if err := decoder.Decode(req, pod); err != nil {
return utils.ErroredResponse(err)
}
tntList := &capsulev1beta2.TenantList{}
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace),
}); err != nil {
return utils.ErroredResponse(err)
}
// the Pod is not running in a Namespace managed by a Tenant
if len(tntList.Items) == 0 {
return nil
}
tnt := tntList.Items[0]
policy := NewPullPolicy(&tnt)
// if Tenant doesn't enforce the pull policy, exit
if policy == nil {
return nil
}
for _, container := range pod.Spec.Containers {
usedPullPolicy := string(container.ImagePullPolicy)
if !policy.IsPolicySupported(usedPullPolicy) {
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenPullPolicy", "Pod %s/%s pull policy %s is forbidden for the current Tenant", req.Namespace, req.Name, usedPullPolicy)
response := admission.Denied(NewImagePullPolicyForbidden(usedPullPolicy, container.Name, policy.AllowedPullPolicies()).Error())
return &response
}
}
return nil
return r.validate(ctx, c, decoder, recorder, req)
}
}
func (r *imagePullPolicy) OnUpdate(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
return func(context.Context, admission.Request) *admission.Response {
return nil
func (r *imagePullPolicy) OnUpdate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
return r.validate(ctx, c, decoder, recorder, req)
}
}
@@ -76,3 +40,73 @@ func (r *imagePullPolicy) OnDelete(client.Client, admission.Decoder, record.Even
return nil
}
}
func (h *imagePullPolicy) validate(
ctx context.Context,
c client.Client,
decoder admission.Decoder,
recorder record.EventRecorder,
req admission.Request,
) *admission.Response {
pod := &corev1.Pod{}
if err := decoder.Decode(req, pod); err != nil {
return utils.ErroredResponse(err)
}
tntList := &capsulev1beta2.TenantList{}
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace),
}); err != nil {
return utils.ErroredResponse(err)
}
if len(tntList.Items) == 0 {
return nil
}
tnt := tntList.Items[0]
policy := NewPullPolicy(&tnt)
if policy == nil {
return nil
}
for _, container := range pod.Spec.InitContainers {
if response := h.verifyPullPolicy(recorder, req, policy, string(container.ImagePullPolicy), container.Name, tnt); response != nil {
return response
}
}
for _, container := range pod.Spec.EphemeralContainers {
if response := h.verifyPullPolicy(recorder, req, policy, string(container.ImagePullPolicy), container.Name, tnt); response != nil {
return response
}
}
for _, container := range pod.Spec.Containers {
if response := h.verifyPullPolicy(recorder, req, policy, string(container.ImagePullPolicy), container.Name, tnt); response != nil {
return response
}
}
return nil
}
func (h *imagePullPolicy) verifyPullPolicy(
recorder record.EventRecorder,
req admission.Request,
policy PullPolicy,
usedPullPolicy string,
container string,
tnt capsulev1beta2.Tenant,
) *admission.Response {
if !policy.IsPolicySupported(usedPullPolicy) {
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenPullPolicy", "Pod %s/%s pull policy %s is forbidden for the current Tenant", req.Namespace, req.Name, usedPullPolicy)
response := admission.Denied(NewImagePullPolicyForbidden(usedPullPolicy, container, policy.AllowedPullPolicies()).Error())
return &response
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More