Compare commits

...

55 Commits

Author SHA1 Message Date
renovate[bot]
2dfd4f39ba chore(deps): update zgosalvez/github-actions-ensure-sha-pinned-actions action to v5 (#1865)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-13 00:28:34 +02:00
renovate[bot]
aa41b27c94 chore(deps): update actions/stale digest to b5d41d4 (#1866)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-13 00:28:10 +02:00
Sander Tervoert
61429d1dae fix(docs): update home in chart.yaml (#1864)
* fix(docs): update home in chart.yaml

Signed-off-by: sandert-k8s <sandert98@gmail.com>

* fix: linter

Signed-off-by: sandert-k8s <sandert98@gmail.com>

---------

Signed-off-by: sandert-k8s <sandert98@gmail.com>
2026-02-10 08:54:08 +01:00
renovate[bot]
a7b437fedb chore(deps): update actions/stale digest to dcd2b94 (#1857)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-06 22:53:08 +02:00
Oliver Bähler
0abc77b56a feat: diverse performance improvements (#1861)
Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2026-02-03 22:05:00 +01:00
renovate[bot]
43c23cb4c8 chore(deps): update github/codeql-action digest to 8aac4e4 (#1855)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 22:14:32 +02:00
renovate[bot]
6d0168add6 chore(deps): update all-ci-updates (#1856)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 22:14:03 +02:00
renovate[bot]
0cb11fc4aa chore(deps): update all-ci-updates (#1837)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-31 11:49:22 +02:00
renovate[bot]
9f625748ed chore(deps): update all-ci-updates (#1851)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 23:33:38 +02:00
renovate[bot]
ab1fa7198a chore(deps): update anchore/sbom-action digest to 5620efe (#1852)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 23:33:21 +02:00
renovate[bot]
21f806c625 chore(deps): update github/codeql-action digest to f985be5 (#1850)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 23:33:07 +02:00
renovate[bot]
03e0202a3d chore(deps): update github/codeql-action digest to b2ff80d (#1821)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 04:04:12 +01:00
renovate[bot]
cc8cb0163e chore(deps): update dependency adrienverge/yamllint to v1.38.0 (#1832)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 04:03:52 +01:00
renovate[bot]
fbf122d79c chore(deps): update anchore/sbom-action digest to deef08a (#1836)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 04:03:33 +01:00
renovate[bot]
a08d6c573e chore(deps): update dependency alessandrojcm/commitlint-pre-commit-hook to v9.24.0 (#1833)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 03:57:52 +01:00
Oliver Bähler
a6b830b1af feat: add ruleset api(#1844)
* fix(controller): decode old object for delete requests

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

* chore: modernize golang

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

* chore: modernize golang

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

* chore: modernize golang

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

* fix(config): remove usergroups default

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

* fix(config): remove usergroups default

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

* sec(ghsa-2ww6-hf35-mfjm): intercept namespace subresource

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

* feat(api): add rulestatus api

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

* chore: conflicts

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

* chore: conflicts

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

* chore: conflicts

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

* chore: conflicts

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

* chore: conflicts

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

* chore: conflicts

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

* chore: conflicts

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

* chore: conflicts

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

* chore: conflicts

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

* chore: conflicts

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

* chore: conflicts

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

* feat(api): add rulestatus api

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

* feat(api): add rulestatus api

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

* feat(api): add rulestatus api

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

* feat(api): add rulestatus api

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

* feat(api): add rulestatus api

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

* feat(api): add rulestatus api

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

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2026-01-27 14:28:48 +01:00
renovate[bot]
b9a14a954d chore(deps): update all-ci-updates (#1848)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 14:40:55 +02:00
renovate[bot]
c4786ec22d chore(deps): update all-ci-updates (#1847)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 18:24:51 +02:00
renovate[bot]
aec29b28ba chore(deps): update actions/checkout action to v6.0.2 (#1845)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 06:00:19 +01:00
renovate[bot]
0fb9f09997 chore(deps): update amannn/action-semantic-pull-request digest to b439535 (#1835)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 11:18:36 +02:00
renovate[bot]
be3ef3b6ed chore(deps): update actions/stale digest to d6f8a33 (#1843)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 11:18:16 +02:00
renovate[bot]
a4ad86ab8a chore(deps): update all-ci-updates (#1830)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-21 14:36:15 +02:00
renovate[bot]
e8bb2380fb fix(deps): update k8s.io/utils digest to 914a6e7 (#1822)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-11 00:11:41 +02:00
renovate[bot]
3b57e6ffd7 chore(deps): update anchore/sbom-action digest to 0b82b0b (#1824)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-11 00:11:20 +02:00
renovate[bot]
45e3a5dbf1 fix(deps): update module github.com/onsi/ginkgo/v2 to v2.27.4 (#1825)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 18:46:09 +02:00
renovate[bot]
2b7656715c chore(deps): update dependency prometheus-operator/prometheus-operator to v0.88.0 (#1828)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 18:45:48 +02:00
renovate[bot]
1f91adc9c8 fix(deps): update module github.com/onsi/gomega to v1.39.0 (#1826)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 13:55:20 +02:00
renovate[bot]
5358b5c085 chore(deps): update helm release kube-prometheus-stack to v80.13.3 (#1827)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 13:54:59 +02:00
renovate[bot]
eb8d2b6076 chore(deps): update dependency golangci/golangci-lint to v2.8.0 (#1823)
* chore(deps): update dependency golangci/golangci-lint to v2.8.0

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

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

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

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

* chore(deps): update dependency golangci/golangci-lint to v2.8.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>
2026-01-08 15:43:32 +02:00
renovate[bot]
6516195264 chore(deps): update dependency prometheus-operator/prometheus-operator to v0.87.1 (#1820)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-08 14:28:36 +02:00
renovate[bot]
70b01d4d18 chore(deps): update helm release kube-prometheus-stack to v80.13.2 (#1817)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-08 13:37:19 +02:00
Oliver Bähler
0515880bf2 feat: use cert-manager certificates by default (#1818)
* fix(controller): decode old object for delete requests

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

* chore: modernize golang

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

* chore: modernize golang

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

* chore: modernize golang

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

* feat(helm): use cert-manager certificates by default

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

* chore: move dependencies to trackable resources

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

* chore: move dependencies to trackable resources

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

* chore: move dependencies to trackable resources

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

* chore: move dependencies to trackable resources

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

* chore: move dependencies to trackable resources

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

* chore: move dependencies to trackable resources

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

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2026-01-06 11:18:56 +01:00
renovate[bot]
26cd514fc2 chore(deps): update github/codeql-action digest to fd448f7 (#1816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-06 09:08:32 +01:00
renovate[bot]
3d0e3ab600 chore(deps): update amannn/action-semantic-pull-request digest to 71b07ef (#1815)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-05 10:58:31 +02:00
renovate[bot]
7f626f1d3a chore(deps): update all-ci-updates (#1814)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-03 10:45:03 +02:00
renovate[bot]
1ecee48c70 chore(deps): update helm release kube-prometheus-stack to v80.9.2 (#1812) 2026-01-01 22:16:58 +02:00
Oliver Bähler
730151cb44 feat: add dynamic capsule user evaluation (#1811)
* chore: improve dev targets

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

* feat(controller): implement deterministic rolebinding reflection

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

* feat(controller): capsule users are determined from configuration status

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

* feat(tenantowners): added agreggate option - tenantowners are always considered capsule users

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

* feat(tenantowner): add implicit aggregation for tenants

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

* chore: remove helm flags

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

* fix(config): remove usergroups default

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

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2025-12-31 11:37:30 +01:00
renovate[bot]
bbbb9a2aa1 chore(deps): update helm release kube-prometheus-stack to v80.8.2 (#1810)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-30 10:42:19 +01:00
renovate[bot]
33e9dd69f5 chore(deps): update anchore/sbom-action digest to a930d0a (#1805)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-30 10:42:02 +01:00
renovate[bot]
aaa3ec42bc fix(deps): update k8s.io/utils digest to 718f0e5 (#1806)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-30 10:41:46 +01:00
renovate[bot]
073070aba8 chore(deps): update all-ci-updates (#1807)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-29 19:14:00 +02:00
Hristo Hristov
370c956040 chore: adjust makefile and releaser for kubernetes 1.35 (#1809)
* chore: adjust makefile and releaser for kubernetes 1.35

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

* chore: adjust makefile and releaser for kubernetes 1.35

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

---------

Signed-off-by: Hristo Hristov <me@hhristov.info>
2025-12-28 18:26:59 +01:00
renovate[bot]
00e3ac68cc chore(deps): update actions/stale digest to a21a081 (#1808)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-27 08:52:46 +01:00
renovate[bot]
9c02af5fbe fix(deps): update k8s.io/utils digest to 383b50a (#1804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 23:54:50 +02:00
renovate[bot]
abc03ad342 fix(deps): update module sigs.k8s.io/cluster-api to v1.12.1 (#1784)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 14:22:09 +02:00
renovate[bot]
768b3340c1 fix(deps): update module github.com/onsi/gomega to v1.38.3 (#1777)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 14:21:43 +02:00
renovate[bot]
5fcf8dd445 chore(deps): update dependency google/ko to v0.18.1 (#1792)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-20 12:06:41 +02:00
renovate[bot]
dbedb159e4 chore(deps): update github/codeql-action digest to f67ec12 (#1790)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-20 12:06:22 +02:00
renovate[bot]
7e2dc68561 fix(deps): update module github.com/onsi/ginkgo/v2 to v2.27.3 (#1776)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-20 12:05:39 +02:00
renovate[bot]
866b600944 chore(deps): update all-ci-updates (#1793)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-20 12:05:14 +02:00
renovate[bot]
53a4f5dcc6 fix(deps): update k8s.io/utils digest to 98d557b (#1803)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-20 12:04:01 +02:00
renovate[bot]
72153ee85d chore(deps): update dependency kubernetes-sigs/controller-tools to v0.20.0 (#1799)
* chore(deps): update dependency kubernetes-sigs/controller-tools to v0.20.0

* chore(deps): update dependency kubernetes-sigs/controller-tools to v0.20.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-12-20 12:03:41 +02:00
renovate[bot]
b8d385229e fix(deps): update module k8s.io/dynamic-resource-allocation to v0.35.0 (#1798)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-20 11:27:33 +02:00
renovate[bot]
7efaa9eb46 fix(deps): update kubernetes packages to v0.35.0 (#1797)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 21:25:50 +02:00
renovate[bot]
87aad43aee chore(deps): update dependency kubernetes-sigs/kind to v0.31.0 (#1796)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 21:25:34 +02:00
428 changed files with 19633 additions and 3984 deletions

View File

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

View File

@@ -15,9 +15,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Ensure SHA pinned actions
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@6124774845927d14c601359ab8138699fa5b70c3 # v4.0.1
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@d5d20e15f2736816ee0e001ba8b24b54d9ffcff4 # v5.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

@@ -16,7 +16,7 @@ jobs:
commit_lint:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: wagoid/commitlint-github-action@b948419dd99f3fd78a6548d48f94e3df7f6bf3ed # v6.2.1

View File

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

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout Code"
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check secret
id: checksecret
uses: ./.github/actions/exists
@@ -47,8 +47,8 @@ jobs:
contents: read
steps:
- name: Checkout Source
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version-file: 'go.mod'
- name: Run Gosec Security Scanner
@@ -56,7 +56,7 @@ jobs:
with:
args: '-no-fail -fmt sarif -out gosec.sarif ./...'
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@c43362b91a940600cde2ebae39ec7a35ad66bdc0
uses: github/codeql-action/upload-sarif@8aac4e47ac8ace7d9e0e0b4ef7407aff0ceb5e87
with:
sarif_file: gosec.sarif
unit_tests:
@@ -64,8 +64,8 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version-file: 'go.mod'
- name: Unit Test

View File

@@ -24,7 +24,7 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: ko build
run: VERSION=${{ github.sha }} make ko-build-all
- name: Trivy Scan Image
@@ -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@c43362b91a940600cde2ebae39ec7a35ad66bdc0
uses: github/codeql-action/upload-sarif@8aac4e47ac8ace7d9e0e0b4ef7407aff0ceb5e87
with:
sarif_file: 'trivy-results.sarif'

View File

@@ -20,7 +20,7 @@ jobs:
capsule-digest: ${{ steps.publish-capsule.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup caches
uses: ./.github/actions/setup-caches
timeout-minutes: 5
@@ -36,7 +36,7 @@ jobs:
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Install Cosign
uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Publish Capsule
id: publish-capsule
uses: peak-scale/github-actions/make-ko-publish@a441cca016861c546ab7e065277e40ce41a3eb84 # v0.2.0

View File

@@ -27,11 +27,11 @@ jobs:
runs-on:
labels: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version-file: 'go.mod'
@@ -45,19 +45,19 @@ jobs:
fail-fast: false
matrix:
k8s-version:
- 'v1.30.0'
- 'v1.31.0'
- 'v1.32.0'
- 'v1.33.0'
- 'v1.34.0'
runs-on:
labels: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ${{ github.event.client_payload.repo }}
ref: ${{ github.event.client_payload.sha }}
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version-file: 'go.mod'

View File

@@ -16,7 +16,7 @@ jobs:
if: github.repository_owner == 'projectcapsule'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: "Extract Version"
id: extract_version
run: |
@@ -45,8 +45,8 @@ jobs:
outputs:
chart-digest: ${{ steps.helm_publish.outputs.digest }}
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: "Extract Version"
id: extract_version
run: |

View File

@@ -23,14 +23,14 @@ jobs:
options: --user root
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run ah lint
working-directory: ./charts/
run: ah lint
lint:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4

View File

@@ -15,10 +15,10 @@ jobs:
name: diff
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version-file: 'go.mod'
- name: Generate manifests
@@ -36,7 +36,7 @@ jobs:
name: yamllint
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install yamllint
run: pip install yamllint
- name: Lint YAML files
@@ -45,8 +45,8 @@ jobs:
name: lint
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version-file: 'go.mod'
- name: Run golangci-lint

View File

@@ -18,11 +18,11 @@ jobs:
id-token: write
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Install Go
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version-file: 'go.mod'
- name: Setup caches
@@ -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@43a17d6e7add2b5535efe4dcae9952337c479a93
- uses: anchore/sbom-action/download-syft@5620efe7f17de3b70cbc020fc49ce9048f1bbacf
- name: Install Cosign
uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
with:

View File

@@ -20,7 +20,7 @@ jobs:
id-token: write
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run analysis
@@ -31,12 +31,12 @@ jobs:
repo_token: ${{ secrets.SCORECARD_READ_TOKEN }}
publish_results: true
- name: Upload artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: SARIF file
path: results.sarif
retention-days: 5
- name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/upload-sarif@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v4.32.1
with:
sarif_file: results.sarif

View File

@@ -15,7 +15,7 @@ jobs:
pull-requests: write
steps:
- name: Close stale pull requests
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f
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.'

1
.gitignore vendored
View File

@@ -8,6 +8,7 @@
bin
dist/
config/
builds/
# Test binary, build with `go test -c`
*.test

View File

@@ -39,7 +39,7 @@ linters:
min-occurrences: 2
goheader:
template: |-
Copyright 2020-2025 Project Capsule Authors
Copyright 2020-2026 Project Capsule Authors
SPDX-License-Identifier: Apache-2.0
inamedparam:
skip-single-param: true

View File

@@ -75,7 +75,7 @@ release:
>
> | Kubernetes version | Minimum required |
> |--------------------|------------------|
> | `v1.34` | `>= 1.34.0` |
> | `v1.35` | `>= 1.35.0` |
Thanks to all the contributors! 🚀 🦄

View File

@@ -1,12 +1,13 @@
nwa:
cmd: "update"
holder: "Project Capsule Authors"
year: "2020-2025"
year: "2020-2026"
spdxids: "Apache-2.0"
path:
- "pkg/**/*.go"
- "cmd/**/*.go"
- "api/**/*.go"
- "internal/**/*.go"
- "controllers/**/*.go"
- "main.go"
mute: false

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
rev: v9.23.0
rev: v9.24.0
hooks:
- id: commitlint
stages: [commit-msg]
@@ -13,7 +13,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/adrienverge/yamllint
rev: v1.37.1
rev: v1.38.0
hooks:
- id: yamllint
args: [-c=.github/configs/lintconf.yaml]

View File

@@ -63,9 +63,8 @@ So the TL;DR answer is:
**Make sure a *KinD* cluster is running on your laptop, and then run `make dev-setup` to setup the dev environment.**. This is not done in the `make dev-setup` setup.
```bash
# If you haven't installed or run `make deploy` before, do it first
# Note: please retry if you saw errors
$ make deploy
# Create a KinD cluster if not already created
$ make dev-cluster
# To retrieve your laptop's IP and execute `make dev-setup` to setup dev env
# For example: LAPTOP_HOST_IP=192.168.10.101 make dev-setup

128
Makefile
View File

@@ -19,7 +19,7 @@ CAPSULE_IMG ?= $(REGISTRY)/$(IMG_BASE)
CLUSTER_NAME ?= capsule
## Kubernetes Version Support
KUBERNETES_SUPPORTED_VERSION ?= "v1.34.0"
KUBERNETES_SUPPORTED_VERSION ?= "v1.35.0"
## Tool Binaries
KUBECTL ?= kubectl
@@ -92,19 +92,47 @@ helm-schema: helm-plugin-schema
helm-test: HELM_KIND_CONFIG ?= ""
helm-test: kind
@mkdir -p /tmp/results || true
@$(KIND) create cluster --wait=60s --name capsule-charts --image kindest/node:$(KUBERNETES_SUPPORTED_VERSION) --config $(HELM_KIND_CONFIG)
@$(KIND) create cluster --wait=60s --name capsule-charts --image kindest/node:$(KUBERNETES_SUPPORTED_VERSION) --config ./hack/kind-cluster.yaml
@make helm-test-exec
@$(KIND) delete cluster --name capsule-charts
helm-test-exec: ct helm-controller-version ko-build-all
$(MAKE) e2e-load-image CLUSTER_NAME=capsule-charts IMAGE=$(CAPSULE_IMG) VERSION=v0.0.0
@$(KUBECTL) create ns capsule-system || true
@$(KUBECTL) apply --force-conflicts --server-side=true -f https://github.com/grafana/grafana-operator/releases/download/v5.18.0/crds.yaml
@$(KUBECTL) apply --force-conflicts --server-side=true -f https://github.com/cert-manager/cert-manager/releases/download/v1.9.1/cert-manager.crds.yaml
@$(KUBECTL) apply --force-conflicts --server-side=true -f https://github.com/prometheus-operator/prometheus-operator/releases/download/v0.58.0/bundle.yaml
$(MAKE) dev-install-deps
$(MAKE) dev-install-grafana-operator-crds
@$(CT) install --config $(SRC_ROOT)/.github/configs/ct.yaml --namespace=capsule-system --all --debug
# Setup development env
dev-build: kind
$(KIND) create cluster --wait=60s --name $(CLUSTER_NAME) --image kindest/node:$(KUBERNETES_SUPPORTED_VERSION) --config ./hack/kind-cluster.yaml
$(MAKE) dev-install-deps
.PHONY: dev-destroy
dev-destroy: kind
$(KIND) delete cluster --name capsule
dev-install-deps: dev-setup-fluxcd dev-setup-cert-manager dev-install-gw-api-crds wait-for-helmreleases
API_GW := none
API_GW_VERSION := v1.3.0
API_GW_LOOKUP := kubernetes-sigs/gateway-api
dev-install-gw-api-crds:
@$(KUBECTL) apply --force-conflicts --server-side=true -f https://github.com/$(API_GW_LOOKUP)/releases/download/$(API_GW_VERSION)/standard-install.yaml
GRAFANA := none
GRAFANA_VERSION := v5.18.0
GRAFANA_LOOKUP := grafana/grafana-operator
dev-install-grafana-operator-crds:
@$(KUBECTL) apply --force-conflicts --server-side=true -f https://github.com/grafana/grafana-operator/releases/download/$(GRAFANA_VERSION)/crds.yaml
PROMETHEUS := none
PROMETHEUS_VERSION := v0.88.0
PROMETHEUS_LOOKUP := prometheus-operator/prometheus-operator
dev-install-prometheus-crds:
@$(KUBECTL) apply --force-conflicts --server-side=true -f https://github.com/prometheus-operator/prometheus-operator/releases/download/$(PROMETHEUS_VERSION)/bundle.yaml
# Usage:
# LAPTOP_HOST_IP=<YOUR_LAPTOP_IP> make dev-setup
# For example:
@@ -127,6 +155,7 @@ IP.1 = $(LAPTOP_HOST_IP)
endef
export TLS_CNF
dev-setup:
$(KUBECTL) -n capsule-system scale deployment capsule-controller-manager --replicas=0 || true
mkdir -p /tmp/k8s-webhook-server/serving-certs
echo "$${TLS_CNF}" > _tls.cnf
openssl req -newkey rsa:4096 -days 3650 -nodes -x509 \
@@ -143,6 +172,7 @@ dev-setup:
export CA_BUNDLE=`openssl base64 -in /tmp/k8s-webhook-server/serving-certs/tls.crt | tr -d '\n'`; \
$(HELM) upgrade \
--dependency-update \
--force-conflicts \
--debug \
--install \
--namespace capsule-system \
@@ -156,8 +186,7 @@ dev-setup:
--set "webhooks.service.url=$${WEBHOOK_URL}" \
--set "webhooks.service.caBundle=$${CA_BUNDLE}" \
capsule \
./charts/capsule
$(KUBECTL) -n capsule-system scale deployment capsule-controller-manager --replicas=0 || true
./charts/capsule || true
setup-monitoring: dev-setup-fluxcd
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/monitoring | envsubst | kubectl apply -f -
@@ -177,9 +206,31 @@ dev-setup-argocd: dev-setup-fluxcd
@printf " \033[1mkubectl get secret -n argocd argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d\033[0m\n\n"
@printf " \033[1mkubectl port-forward svc/argocd-server 9091:80 -n argocd\033[0m\n\n"
dev-setup-cert-manager:
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/cert-manager | envsubst | kubectl apply -f -
dev-setup-fluxcd:
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/fluxcd | envsubst | kubectl apply -f -
# Here to setup the current capsule version
# Intended to test updates to new version
dev-setup-capsule: dev-setup-fluxcd
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/capsule | envsubst | kubectl apply -f -
@$(MAKE) wait-for-helmreleases
@$(MAKE) dev-setup-capsule-example
dev-setup-capsule-example: dev-setup-fluxcd
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/capsule/example-setup | envsubst | kubectl apply -f -
@$(KUBECTL) create ns wind-test --as joe --as-group projectcapsule.dev || true
@$(KUBECTL) create ns wind-prod --as joe --as-group projectcapsule.dev || true
@$(KUBECTL) create ns green-test --as bob --as-group projectcapsule.dev || true
@$(KUBECTL) create ns green-prod --as bob --as-group projectcapsule.dev || true
@$(KUBECTL) create ns solar-test --as alice --as-group projectcapsule.dev || true
@$(KUBECTL) create ns solar-prod --as alice --as-group projectcapsule.dev || true
@$(KUBECTL) apply -f hack/distro/capsule/example-setup/claims.yaml
wait-for-helmreleases:
@ echo "Waiting for all HelmReleases to have observedGeneration >= 0..."
@while [ "$$($(KUBECTL) get helmrelease -A -o jsonpath='{range .items[?(@.status.observedGeneration<0)]}{.metadata.namespace}{" "}{.metadata.name}{"\n"}{end}' | wc -l)" -ne 0 ]; do \
@@ -187,6 +238,42 @@ wait-for-helmreleases:
done
ENTERPRISE_VERSION ?= "0.13.0-rc.2"
ENTERPRISE_REGISTRY ?= "oci.peakscale.ch"
enterprise-prerelease:
mkdir -p ./builds
$(MAKE) CAPSULE_IMG=$(ENTERPRISE_REGISTRY)/prereleases/images/capsule VERSION=$(ENTERPRISE_VERSION) ko-publish-capsule
$(HELM) package ./charts/capsule --app-version=$(ENTERPRISE_VERSION) --version=$(ENTERPRISE_VERSION) --destination ./builds/
$(HELM) push ./builds/capsule-$(ENTERPRISE_VERSION).tgz oci://$(ENTERPRISE_REGISTRY)/prereleases/charts/
$(MAKE) deploy-enterprise
rm -rf ./builds
deploy-enterprise:
@echo ""
@echo "Deploying Capsule Prerelease (Enterprise) $(ENTERPRISE_VERSION)"
@echo ""
@echo "1) Create image pull secret (Change the credentials with the ones provided to you):"
@echo ""
@echo "kubectl create secret docker-registry capsule-enterprise -n capsule-system \\"
@echo " --docker-username='robot\$$name' \\"
@echo " --docker-password='serviceaccount-password' \\"
@echo " --docker-server='$(ENTERPRISE_REGISTRY)'"
@echo ""
@echo "2) Deploy Capsule:"
@echo ""
@echo "helm upgrade --install capsule \\"
@echo " oci://$(ENTERPRISE_REGISTRY)/prereleases/charts/capsule \\"
@echo " --namespace capsule-system \\"
@echo " --version $(ENTERPRISE_VERSION) \\"
@echo " --reuse-values \\"
@echo " --set manager.image.registry=$(ENTERPRISE_REGISTRY) \\"
@echo " --set manager.image.repository=prereleases/images/capsule \\"
@echo " --set manager.image.tag=$(ENTERPRISE_VERSION) \\"
@echo " --set manager.image.pullPolicy=Always \\"
@echo " --set 'serviceAccount.imagePullSecrets={capsule-enterprise}'"
@echo ""
####################
# -- Docker
####################
@@ -264,22 +351,12 @@ golint-fix: golangci-lint
e2e: ginkgo
$(MAKE) e2e-build && $(MAKE) e2e-exec && $(MAKE) e2e-destroy
API_GW := none
API_GW_VERSION := v1.3.0
API_GW_LOOKUP := kubernetes-sigs/gateway-api/
e2e-install-deps:
@$(KUBECTL) apply --force-conflicts --server-side=true -f https://github.com/$(API_GW_LOOKUP)/releases/download/$(API_GW_VERSION)/standard-install.yaml
e2e-build: kind
$(MAKE) e2e-build-cluster
$(MAKE) dev-build
$(MAKE) e2e-install
e2e-build-cluster: kind
$(KIND) create cluster --wait=60s --name $(CLUSTER_NAME) --image kindest/node:$(KUBERNETES_SUPPORTED_VERSION)
$(MAKE) e2e-install-deps
.PHONY: e2e-install
e2e-install: ko-build-all
e2e-install: helm-controller-version ko-build-all
$(MAKE) e2e-load-image CLUSTER_NAME=$(CLUSTER_NAME) IMAGE=$(CAPSULE_IMG) VERSION=$(VERSION)
$(HELM) upgrade \
--dependency-update \
@@ -287,12 +364,14 @@ e2e-install: ko-build-all
--install \
--namespace capsule-system \
--create-namespace \
--set 'replicaCount=2'\
--set 'manager.image.pullPolicy=Never' \
--set 'manager.resources=null'\
--set "manager.image.tag=$(VERSION)" \
--set 'manager.livenessProbe.failureThreshold=10' \
--set 'webhooks.hooks.nodes.enabled=true' \
--set "webhooks.exclusive=true"\
--set "manager.options.logLevel=debug"\
capsule \
./charts/capsule
@@ -339,8 +418,7 @@ e2e-exec: ginkgo
$(GINKGO) -v -tags e2e ./e2e
.PHONY: e2e-destroy
e2e-destroy: kind
$(KIND) delete cluster --name capsule
e2e-destroy: dev-destroy
SPELL_CHECKER = npx spellchecker-cli
docs-lint:
@@ -377,7 +455,7 @@ helm-doc:
# -- Tools
####################
CONTROLLER_GEN := $(LOCALBIN)/controller-gen
CONTROLLER_GEN_VERSION ?= v0.19.0
CONTROLLER_GEN_VERSION ?= v0.20.0
CONTROLLER_GEN_LOOKUP := kubernetes-sigs/controller-tools
controller-gen:
@test -s $(CONTROLLER_GEN) && $(CONTROLLER_GEN) --version | grep -q $(CONTROLLER_GEN_VERSION) || \
@@ -395,14 +473,14 @@ ct:
$(call go-install-tool,$(CT),github.com/$(CT_LOOKUP)/v3/ct@$(CT_VERSION))
KIND := $(LOCALBIN)/kind
KIND_VERSION := v0.30.0
KIND_VERSION := v0.31.0
KIND_LOOKUP := kubernetes-sigs/kind
kind:
@test -s $(KIND) && $(KIND) --version | grep -q $(KIND_VERSION) || \
$(call go-install-tool,$(KIND),sigs.k8s.io/kind/cmd/kind@$(KIND_VERSION))
KO := $(LOCALBIN)/ko
KO_VERSION := v0.18.0
KO_VERSION := v0.18.1
KO_LOOKUP := google/ko
ko:
@test -s $(KO) && $(KO) -h | grep -q $(KO_VERSION) || \
@@ -416,7 +494,7 @@ nwa:
$(call go-install-tool,$(NWA),github.com/$(NWA_LOOKUP)@$(NWA_VERSION))
GOLANGCI_LINT := $(LOCALBIN)/golangci-lint
GOLANGCI_LINT_VERSION := v2.7.2
GOLANGCI_LINT_VERSION := v2.8.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

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta1

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta1

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
// Package v1beta1 contains API Schema definitions for the capsule v1beta1 API group

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta1

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta1

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta1

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta1

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta1

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta1

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta1

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta1

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta1

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta1

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta1

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta1
@@ -15,7 +15,6 @@ func (in *Tenant) SetupWebhookWithManager(mgr ctrl.Manager) error {
return nil
}
return ctrl.NewWebhookManagedBy(mgr).
For(in).
return ctrl.NewWebhookManagedBy(mgr, in).
Complete()
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2

View File

@@ -0,0 +1,19 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/projectcapsule/capsule/pkg/api"
)
// CapsuleConfigurationStatus defines the Capsule configuration status.
type CapsuleConfigurationStatus struct {
// Last time all caches were invalided
LastCacheInvalidation metav1.Time `json:"lastCacheInvalidation,omitempty"`
// Users which are considered Capsule Users and are bound to the Capsule Tenant construct.
Users api.UserListSpec `json:"users,omitempty"`
}

View File

@@ -1,12 +1,14 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
)
// CapsuleConfigurationSpec defines the Capsule configuration.
@@ -21,7 +23,6 @@ type CapsuleConfigurationSpec struct {
// Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users)
//
// Names of the groups considered as Capsule users.
// +kubebuilder:default={capsule.clastix.io}
UserGroups []string `json:"userGroups,omitempty"`
// 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.
@@ -54,6 +55,50 @@ type CapsuleConfigurationSpec struct {
// for interacting with namespaces. Because if that label is not defined, it's assumed that namespace interaction was not targeted towards a tenant and will therefor
// be ignored by capsule.
Administrators api.UserListSpec `json:"administrators,omitempty"`
// Configuration for dynamic Validating and Mutating Admission webhooks managed by Capsule.
Admission DynamicAdmission `json:"admission,omitempty"`
// Define Properties for managed ClusterRoles by Capsule
// +kubebuilder:default={}
RBAC *RBACConfiguration `json:"rbac"`
// Define the period of time upon a cache invalidation is executed for all caches.
// +kubebuilder:default="24h"
CacheInvalidation metav1.Duration `json:"cacheInvalidation"`
}
type RBACConfiguration struct {
// The ClusterRoles applied for Administrators
// +kubebuilder:default={capsule-namespace-deleter}
AdministrationClusterRoles []string `json:"administrationClusterRoles,omitempty"`
// The ClusterRoles applied for ServiceAccounts which had owner Promotion
// +kubebuilder:default={capsule-namespace-provisioner,capsule-namespace-deleter}
PromotionClusterRoles []string `json:"promotionClusterRoles,omitempty"`
// Name for the ClusterRole required to grant Namespace Deletion permissions.
// +kubebuilder:default=capsule-namespace-deleter
DeleterClusterRole string `json:"deleter,omitempty"`
// Name for the ClusterRole required to grant Namespace Provision permissions.
// +kubebuilder:default=capsule-namespace-provisioner
ProvisionerClusterRole string `json:"provisioner,omitempty"`
}
type DynamicAdmission struct {
// Configure dynamic Mutating Admission for Capsule
Mutating DynamicAdmissionConfig `json:"mutating,omitempty"`
// Configure dynamic Validating Admission for Capsule
Validating DynamicAdmissionConfig `json:"validating,omitempty"`
}
type DynamicAdmissionConfig struct {
// Name the Admission Webhook
Name meta.RFC1123Name `json:"name,omitempty"`
// Labels added to the Admission Webhook
// +optional
Labels map[string]string `json:"labels,omitempty"`
// Annotations added to the Admission Webhook
// +optional
Annotations map[string]string `json:"annotations,omitempty"`
// From the upstram struct
Client admissionregistrationv1.WebhookClientConfig `json:"client"`
}
type NodeMetadata struct {
@@ -79,6 +124,7 @@ type CapsuleResources struct {
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:storageversion
@@ -90,6 +136,9 @@ type CapsuleConfiguration struct {
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec CapsuleConfigurationSpec `json:"spec"`
// +optional
Status CapsuleConfigurationStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
// Package v1beta2 contains API Schema definitions for the capsule v1beta2 API group

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
@@ -17,6 +17,9 @@ type NamespaceOptions struct {
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"`
// Required Metadata for namespace within this tenant
// +optional
RequiredMetadata *RequiredMetadata `json:"requiredMetadata,omitzero"`
// Define the labels that a Tenant Owner cannot set for their Namespace resources.
// +optional
ForbiddenLabels api.ForbiddenListSpec `json:"forbiddenLabels,omitzero"`
@@ -27,3 +30,13 @@ type NamespaceOptions struct {
//+kubebuilder:default:=false
ManagedMetadataOnly bool `json:"managedMetadataOnly,omitempty"`
}
type RequiredMetadata struct {
// Labels that must be defined for each namespace
// +optional
Labels map[string]string `json:"labels,omitzero"`
// Annotations that must be defined for each namespace
// +optional
Annotations map[string]string `json:"annotations,omitzero"`
}

View File

@@ -0,0 +1,33 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/projectcapsule/capsule/pkg/api"
)
// +kubebuilder:object:generate=true
type NamespaceRule struct {
// Enforce these properties via Rules
NamespaceRuleBody `json:",inline"`
// Select namespaces which are going to usese
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
}
// +kubebuilder:object:generate=true
type NamespaceRuleBody struct {
// Enforcement Rules applied
//+optional
Enforce NamespaceRuleEnforceBody `json:"enforce,omitzero"`
}
// +kubebuilder:object:generate=true
type NamespaceRuleEnforceBody struct {
// Define registries which are allowed to be used within this tenant
// The rules are aggregated, since you can use Regular Expressions the match registry endpoints
Registries []api.OCIRegistry `json:"registries,omitempty"`
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
@@ -11,7 +11,7 @@ import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
)
func (r *ResourcePool) GetQuotaName() string {
@@ -79,9 +79,10 @@ func (r *ResourcePool) AddClaimToStatus(claim *ResourcePoolClaim) {
}
scl := &ResourcePoolClaimsItem{
StatusNameUID: api.StatusNameUID{
UID: claim.UID,
Name: api.Name(claim.Name),
NamespacedRFC1123ObjectReferenceWithNamespaceWithUID: meta.NamespacedRFC1123ObjectReferenceWithNamespaceWithUID{
UID: claim.UID,
Name: meta.RFC1123Name(claim.Name),
Namespace: meta.RFC1123SubdomainName(claim.Namespace),
},
Claims: claim.Spec.ResourceClaims,
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
@@ -11,7 +11,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/stretchr/testify/assert"
)
@@ -34,7 +33,7 @@ func TestGetClaimFromStatus(t *testing.T) {
Claims: ResourcePoolNamespaceClaimsStatus{
ns: {
&ResourcePoolClaimsItem{
StatusNameUID: api.StatusNameUID{
NamespacedRFC1123ObjectReferenceWithNamespaceWithUID: meta.NamespacedRFC1123ObjectReferenceWithNamespaceWithUID{
UID: testUID,
},
Claims: corev1.ResourceList{
@@ -127,8 +126,9 @@ func TestAddRemoveClaimToStatus(t *testing.T) {
pool.AddClaimToStatus(claim)
stored := pool.GetClaimFromStatus(claim)
assert.NotNil(t, stored)
assert.Equal(t, api.Name("claim-1"), stored.Name)
assert.Equal(t, meta.RFC1123Name("claim-1"), stored.Name)
pool.RemoveClaimFromStatus(claim)
assert.Nil(t, pool.GetClaimFromStatus(claim))
@@ -219,7 +219,7 @@ func TestGetNamespaceClaims(t *testing.T) {
Claims: ResourcePoolNamespaceClaimsStatus{
"ns": {
&ResourcePoolClaimsItem{
StatusNameUID: api.StatusNameUID{UID: "uid1"},
NamespacedRFC1123ObjectReferenceWithNamespaceWithUID: meta.NamespacedRFC1123ObjectReferenceWithNamespaceWithUID{UID: "uid1"},
Claims: corev1.ResourceList{
corev1.ResourceLimitsCPU: resource.MustParse("1"),
},
@@ -260,36 +260,59 @@ func TestIsBoundToResourcePool_2(t *testing.T) {
t.Run("bound to resource pool (Assigned=True)", func(t *testing.T) {
claim := &ResourcePoolClaim{
Status: ResourcePoolClaimStatus{
Condition: metav1.Condition{
Type: meta.BoundCondition,
Status: metav1.ConditionTrue,
},
Conditions: meta.ConditionList{},
},
}
assert.Equal(t, true, claim.IsBoundToResourcePool())
assert.Equal(t, false, claim.IsBoundInResourcePool())
})
t.Run("not bound - wrong condition type", func(t *testing.T) {
claim := &ResourcePoolClaim{
Status: ResourcePoolClaimStatus{
Condition: metav1.Condition{
Type: "Other",
Status: metav1.ConditionTrue,
Conditions: meta.ConditionList{
meta.Condition{},
},
},
}
assert.Equal(t, false, claim.IsBoundToResourcePool())
cond := meta.NewAssignedCondition(claim)
cond.Status = metav1.ConditionFalse
claim.Status.Conditions.UpdateConditionByType(cond)
assert.Equal(t, false, claim.IsBoundInResourcePool())
})
t.Run("not bound - condition not true", func(t *testing.T) {
claim := &ResourcePoolClaim{
Status: ResourcePoolClaimStatus{
Condition: metav1.Condition{
Type: meta.BoundCondition,
Status: metav1.ConditionFalse,
Conditions: meta.ConditionList{
meta.Condition{},
},
},
}
assert.Equal(t, false, claim.IsBoundToResourcePool())
cond := meta.NewBoundCondition(claim)
cond.Status = metav1.ConditionFalse
claim.Status.Conditions.UpdateConditionByType(cond)
assert.Equal(t, false, claim.IsBoundInResourcePool())
})
t.Run("not bound - condition not true", func(t *testing.T) {
claim := &ResourcePoolClaim{
Status: ResourcePoolClaimStatus{
Conditions: meta.ConditionList{
meta.Condition{},
},
},
}
cond := meta.NewBoundCondition(claim)
cond.Status = metav1.ConditionTrue
claim.Status.Conditions.UpdateConditionByType(cond)
assert.Equal(t, true, claim.IsBoundInResourcePool())
})
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
@@ -8,6 +8,7 @@ import (
"k8s.io/apimachinery/pkg/types"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
)
// GlobalResourceQuotaStatus defines the observed state of GlobalResourceQuota.
@@ -28,6 +29,8 @@ type ResourcePoolStatus struct {
Allocation ResourcePoolQuotaStatus `json:"allocation,omitzero"`
// Exhaustions from claims associated with the pool
Exhaustions map[string]api.PoolExhaustionResource `json:"exhaustions,omitempty"`
// Conditions for the resource claim
Conditions meta.ConditionList `json:"conditions,omitzero"`
}
type ResourcePoolNamespaceClaimsStatus map[string]ResourcePoolClaimsList
@@ -60,7 +63,7 @@ func (r *ResourcePoolClaimsList) GetClaimByUID(uid types.UID) *ResourcePoolClaim
// ResourceQuotaClaimStatus defines the observed state of ResourceQuotaClaim.
type ResourcePoolClaimsItem struct {
// Reference to the GlobalQuota being claimed from
api.StatusNameUID `json:",inline"`
meta.NamespacedRFC1123ObjectReferenceWithNamespaceWithUID `json:",inline"`
// Claimed resources
Claims corev1.ResourceList `json:"claims,omitempty"`

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
@@ -7,13 +7,13 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/projectcapsule/capsule/pkg/api/misc"
"github.com/projectcapsule/capsule/pkg/runtime/selectors"
)
// ResourcePoolSpec.
type ResourcePoolSpec struct {
// Selector to match the namespaces that should be managed by the GlobalResourceQuota
Selectors []misc.NamespaceSelector `json:"selectors,omitempty"`
Selectors []selectors.NamespaceSelector `json:"selectors,omitempty"`
// Define the resourcequota served by this resourcepool.
Quota corev1.ResourceQuotaSpec `json:"quota"`
// The Defaults given for each namespace, the default is not counted towards the total allocation
@@ -27,7 +27,7 @@ type ResourcePoolSpec struct {
}
type ResourcePoolSpecConfiguration struct {
// With this option all resources which can be allocated are set to 0 for the resourcequota defaults.
// With this option all resources which can be allocated are set to 0 for the resourcequota defaults. (Default false)
// +kubebuilder:default=false
DefaultsAssignZero *bool `json:"defaultsZero,omitempty"`
// Claims are queued whenever they are allocated to a pool. A pool tries to allocate claims in order based on their
@@ -35,11 +35,11 @@ type ResourcePoolSpecConfiguration struct {
// but if a lower priority claim still has enough space in the available resources, it will be able to claim them. Eventough
// it's priority was lower
// Enabling this option respects to Order. Meaning the Creationtimestamp matters and if a resource is put into the queue, no
// other claim can claim the same resources with lower priority.
// other claim can claim the same resources with lower priority. (Default false)
// +kubebuilder:default=false
OrderedQueue *bool `json:"orderedQueue,omitempty"`
// When a resourcepool is deleted, the resourceclaims bound to it are disassociated from the resourcepool but not deleted.
// By Enabling this option, the resourceclaims will be deleted when the resourcepool is deleted, if they are in bound state.
// By Enabling this option, the resourceclaims will be deleted when the resourcepool is deleted, if they are in bound state. (Default false)
// +kubebuilder:default=false
DeleteBoundResources *bool `json:"deleteBoundResources,omitempty"`
}
@@ -49,6 +49,8 @@ type ResourcePoolSpecConfiguration struct {
// +kubebuilder:resource:scope=Cluster,shortName=quotapool
// +kubebuilder:printcolumn:name="Claims",type="integer",JSONPath=".status.claimCount",description="The total amount of Claims bound"
// +kubebuilder:printcolumn:name="Namespaces",type="integer",JSONPath=".status.namespaceCount",description="The total amount of Namespaces considered"
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Reconcile Status"
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="Reconcile Message"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
// Resourcepools allows you to define a set of resources as known from ResoureQuotas. The Resourcepools are defined at cluster-scope an should

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
@@ -10,11 +10,52 @@ import (
)
// Indicate the claim is bound to a resource pool.
func (r *ResourcePoolClaim) IsBoundToResourcePool() bool {
if r.Status.Condition.Type == meta.BoundCondition &&
r.Status.Condition.Status == metav1.ConditionTrue {
func (r *ResourcePoolClaim) IsExhaustedInResourcePool() bool {
condition := r.Status.Conditions.GetConditionByType(meta.ExhaustedCondition)
if condition == nil {
return false
}
if condition.Status == metav1.ConditionTrue {
return true
}
return false
}
func (r *ResourcePoolClaim) IsAssignedInResourcePool() bool {
condition := r.Status.Conditions.GetConditionByType(meta.AssignedCondition)
if condition == nil {
return false
}
if condition.Status == metav1.ConditionTrue {
return true
}
return false
}
func (r *ResourcePoolClaim) IsBoundInResourcePool() bool {
condition := r.Status.Conditions.GetConditionByType(meta.BoundCondition)
if condition == nil {
return false
}
if condition.Status == metav1.ConditionTrue {
return true
}
return false
}
func (r *ResourcePoolClaim) GetPool() string {
if name := string(r.Status.Pool.Name); name != "" {
return name
}
return r.Spec.Pool
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
@@ -6,9 +6,10 @@ package v1beta2
import (
"testing"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/projectcapsule/capsule/pkg/api/meta"
)
func TestIsBoundToResourcePool(t *testing.T) {
@@ -21,9 +22,14 @@ func TestIsBoundToResourcePool(t *testing.T) {
name: "bound to resource pool (Assigned=True)",
claim: ResourcePoolClaim{
Status: ResourcePoolClaimStatus{
Condition: metav1.Condition{
Type: meta.BoundCondition,
Status: metav1.ConditionTrue,
Conditions: meta.ConditionList{
meta.Condition{
Type: meta.BoundCondition,
Status: metav1.ConditionTrue,
Reason: meta.SucceededReason,
Message: "reconciled",
LastTransitionTime: metav1.Now(),
},
},
},
},
@@ -33,9 +39,14 @@ func TestIsBoundToResourcePool(t *testing.T) {
name: "not bound - wrong condition type",
claim: ResourcePoolClaim{
Status: ResourcePoolClaimStatus{
Condition: metav1.Condition{
Type: "SomethingElse",
Status: metav1.ConditionTrue,
Conditions: meta.ConditionList{
meta.Condition{
Type: meta.AssignedCondition,
Status: metav1.ConditionTrue,
Reason: meta.SucceededReason,
Message: "reconciled",
LastTransitionTime: metav1.Now(),
},
},
},
},
@@ -45,9 +56,14 @@ func TestIsBoundToResourcePool(t *testing.T) {
name: "not bound - status not true",
claim: ResourcePoolClaim{
Status: ResourcePoolClaimStatus{
Condition: metav1.Condition{
Type: meta.BoundCondition,
Status: metav1.ConditionFalse,
Conditions: meta.ConditionList{
meta.Condition{
Type: meta.AssignedCondition,
Status: metav1.ConditionTrue,
Reason: meta.SucceededReason,
Message: "reconciled",
LastTransitionTime: metav1.Now(),
},
},
},
},
@@ -64,7 +80,91 @@ func TestIsBoundToResourcePool(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := tt.claim.IsBoundToResourcePool()
actual := tt.claim.IsBoundInResourcePool()
assert.Equal(t, tt.expected, actual)
})
}
}
func TestGetPool(t *testing.T) {
tests := []struct {
name string
claim ResourcePoolClaim
expected string
}{
{
name: "returns status pool name when set",
claim: ResourcePoolClaim{
Spec: ResourcePoolClaimSpec{
Pool: "spec-pool",
},
Status: ResourcePoolClaimStatus{
Pool: meta.LocalRFC1123ObjectReferenceWithUID{
Name: meta.RFC1123Name("status-pool"),
},
},
},
expected: "status-pool",
},
{
name: "falls back to spec pool when status pool name is empty",
claim: ResourcePoolClaim{
Spec: ResourcePoolClaimSpec{
Pool: "spec-pool",
},
Status: ResourcePoolClaimStatus{
Pool: meta.LocalRFC1123ObjectReferenceWithUID{
Name: meta.RFC1123Name(""),
},
},
},
expected: "spec-pool",
},
{
name: "falls back to spec pool when status pool struct is zero-value",
claim: ResourcePoolClaim{
Spec: ResourcePoolClaimSpec{
Pool: "spec-pool",
},
Status: ResourcePoolClaimStatus{
Pool: meta.LocalRFC1123ObjectReferenceWithUID{},
},
},
expected: "spec-pool",
},
{
name: "returns empty when both status and spec are empty",
claim: ResourcePoolClaim{
Spec: ResourcePoolClaimSpec{
Pool: "",
},
Status: ResourcePoolClaimStatus{
Pool: meta.LocalRFC1123ObjectReferenceWithUID{
Name: meta.RFC1123Name(""),
},
},
},
expected: "",
},
{
name: "status wins even if spec differs",
claim: ResourcePoolClaim{
Spec: ResourcePoolClaimSpec{
Pool: "spec-pool",
},
Status: ResourcePoolClaimStatus{
Pool: meta.LocalRFC1123ObjectReferenceWithUID{
Name: meta.RFC1123Name("status-pool"),
},
},
},
expected: "status-pool",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := tt.claim.GetPool()
assert.Equal(t, tt.expected, actual)
})
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
@@ -7,7 +7,7 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
)
type ResourcePoolClaimSpec struct {
@@ -23,20 +23,26 @@ type ResourcePoolClaimSpec struct {
type ResourcePoolClaimStatus struct {
// Reference to the GlobalQuota being claimed from
// +optional
Pool api.StatusNameUID `json:"pool,omitzero"`
// Condtion for this resource claim
Pool meta.LocalRFC1123ObjectReferenceWithUID `json:"pool,omitzero"`
// Deprecated: Use Conditions
//
// +optional
Condition metav1.Condition `json:"condition,omitzero"`
// Conditions for the resource claim
Conditions meta.ConditionList `json:"conditions,omitzero"`
// Tracks the Usage from Claimed from this claim and available resources
// +optional
Allocation ResourcePoolQuotaStatus `json:"allocation,omitzero"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Pool",type="string",JSONPath=".status.pool.name",description="The ResourcePool being claimed from"
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.condition.type",description="Status for claim"
// +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.condition.reason",description="Reason for status"
// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.condition.message",description="Condition Message"
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Ready Status"
// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="Ready Message"
// +kubebuilder:printcolumn:name="Bound",type="string",JSONPath=".status.conditions[?(@.type==\"Bound\")].status",description="Bound Status"
// +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.conditions[?(@.type==\"Bound\")].message",description="Bound Message"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
// ResourcePoolClaim is the Schema for the resourcepoolclaims API.
type ResourcePoolClaim struct {
metav1.TypeMeta `json:",inline"`
@@ -51,7 +57,6 @@ type ResourcePoolClaim struct {
}
// +kubebuilder:object:root=true
// ResourceQuotaClaimList contains a list of ResourceQuotaClaim.
type ResourcePoolClaimList struct {
metav1.TypeMeta `json:",inline"`

View File

@@ -0,0 +1,44 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
type RuleStatus struct {
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
// +optional
Status RuleStatusSpec `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
// RuleStatusList contains a list of RuleStatus.
type RuleStatusList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []RuleStatus `json:"items"`
}
func init() {
SchemeBuilder.Register(&RuleStatus{}, &RuleStatusList{})
}
// RuleStatus contains the accumulated rules applying to namespace it's deployed in.
// +kubebuilder:object:generate=true
type RuleStatusSpec struct {
// Managed Enforcement properties per Namespace (aggregated from rules)
//+optional
Rule NamespaceRuleBody `json:"rule,omitzero"`
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2

View File

@@ -1,77 +1,28 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
import (
"context"
"slices"
"sort"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apiserver/pkg/authentication/serviceaccount"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
)
func (in *Tenant) CollectOwners(ctx context.Context, c client.Client, allowPromotion bool, admins api.UserListSpec) (api.OwnerStatusListSpec, error) {
owners := in.Spec.Owners.ToStatusOwners()
func (in *Tenant) GetRoleBindings() []api.AdditionalRoleBindingsSpec {
roleBindings := make([]api.AdditionalRoleBindingsSpec, 0, len(in.Spec.AdditionalRoleBindings))
// Promoted ServiceAccounts
if allowPromotion && len(in.Status.Namespaces) > 0 {
saList := &corev1.ServiceAccountList{}
if err := c.List(ctx, saList,
client.MatchingLabels{
meta.OwnerPromotionLabel: meta.OwnerPromotionLabelTrigger,
},
); err != nil {
return nil, err
}
for _, sa := range saList.Items {
for _, ns := range in.Status.Namespaces {
if sa.GetNamespace() != ns {
continue
}
owners.Upsert(api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.ServiceAccountOwner,
Name: serviceaccount.ServiceAccountUsernamePrefix + sa.Namespace + ":" + sa.Name,
},
ClusterRoles: []string{
api.ProvisionerRoleName,
api.DeleterRoleName,
},
})
}
}
for _, owner := range in.Status.Owners {
roleBindings = append(roleBindings, owner.ToAdditionalRolebindings()...)
}
// Administrators
for _, a := range admins {
owners.Upsert(api.CoreOwnerSpec{
UserSpec: a,
ClusterRoles: []string{
api.DeleterRoleName,
},
})
}
roleBindings = append(roleBindings, in.Spec.AdditionalRoleBindings...)
// Dedicated Owner Objects
listed, err := in.Spec.Permissions.ListMatchingOwners(ctx, c)
if err != nil {
return nil, err
}
for _, o := range listed {
owners.Upsert(o.Spec.CoreOwnerSpec)
}
return owners, nil
return roleBindings
}
func (in *Tenant) IsFull() bool {

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
@@ -47,6 +47,14 @@ type TenantStatusNamespaceItem struct {
UID k8stypes.UID `json:"uid,omitempty"`
// Managed Metadata
Metadata *TenantStatusNamespaceMetadata `json:"metadata,omitempty"`
// Managed Metadata
//+optional
Enforce TenantStatusNamespaceEnforcement `json:"enforce,omitzero"`
}
type TenantStatusNamespaceEnforcement struct {
// Registries which are allowed within this namespace
Registries []api.OCIRegistry `json:"registry,omitempty"`
}
type TenantStatusNamespaceMetadata struct {

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
@@ -10,7 +10,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/misc"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/runtime/selectors"
)
// TenantSpec defines the desired state of Tenant.
@@ -18,6 +19,14 @@ type TenantSpec struct {
// Specify Permissions for the Tenant.
// +optional
Permissions Permissions `json:"permissions,omitzero"`
// Specify enforcement specifications for the scope of the Tenant.
// We are moving all configuration enforcement. per namespace into a rule construct.
// It's currently not final.
//
// Read More: https://projectcapsule.dev/docs/tenants/rules/
//+optional
Rules []*NamespaceRule `json:"rules,omitzero"`
// Specifies the owners of the Tenant.
// Optional
Owners api.OwnerListSpec `json:"owners,omitempty"`
@@ -35,27 +44,13 @@ type TenantSpec struct {
// Specifies options for the Ingress resources, such as allowed hostnames and IngressClass. Optional.
// +optional
IngressOptions IngressOptions `json:"ingressOptions,omitzero"`
// Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
ContainerRegistries *api.AllowedListSpec `json:"containerRegistries,omitempty"`
// 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"`
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
//
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
// +optional
NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitzero"`
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
//
// Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
// +optional
LimitRanges api.LimitRangesSpec `json:"limitRanges,omitzero"`
// 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.
// +optional
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitzero"`
// Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
AdditionalRoleBindings []api.AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
// Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
ImagePullPolicies []api.ImagePullPolicySpec `json:"imagePullPolicies,omitempty"`
// Specifies the allowed RuntimeClasses assigned to the Tenant.
// Capsule assures that all Pods resources created in the Tenant can use only one of the allowed RuntimeClasses.
// Optional.
@@ -86,6 +81,26 @@ type TenantSpec struct {
// If unset, Tenant uses CapsuleConfiguration's forceTenantPrefix
// Optional
ForceTenantPrefix *bool `json:"forceTenantPrefix,omitempty"`
// Deprecated: Use Enforcement.Registries instead
//
// Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
ContainerRegistries *api.AllowedListSpec `json:"containerRegistries,omitempty"`
// Deprecated: Use Enforcement.Registries instead
//
// Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
ImagePullPolicies []api.ImagePullPolicySpec `json:"imagePullPolicies,omitempty"`
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
//
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
// +optional
NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitzero"`
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
//
// Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
// +optional
LimitRanges api.LimitRangesSpec `json:"limitRanges,omitzero"`
}
type Permissions struct {
@@ -98,9 +113,16 @@ type Permissions struct {
func (p *Permissions) ListMatchingOwners(
ctx context.Context,
c client.Client,
tnt string,
opts ...client.ListOption,
) ([]*TenantOwner, error) {
return misc.ListBySelectors[*TenantOwner](ctx, c, &TenantOwnerList{}, p.MatchOwners)
defaultSelector := &metav1.LabelSelector{
MatchLabels: map[string]string{
meta.NewTenantLabel: tnt,
},
}
return selectors.ListBySelectors[*TenantOwner](ctx, c, &TenantOwnerList{}, append(p.MatchOwners, defaultSelector))
}
// +kubebuilder:object:root=true
@@ -121,18 +143,15 @@ type Tenant struct {
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec TenantSpec `json:"spec"`
// +optional
Spec TenantSpec `json:"spec,omitzero"`
// +optional
Status TenantStatus `json:"status,omitzero"`
}
func (in *Tenant) GetNamespaces() (res []string) {
res = make([]string, 0, len(in.Status.Namespaces))
res = append(res, in.Status.Namespaces...)
return res
return in.Status.Namespaces
}
// +kubebuilder:object:root=true

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2
@@ -11,7 +11,14 @@ import (
// TenantOwnerSpec defines the desired state of TenantOwner.
type TenantOwnerSpec struct {
// Subject
api.CoreOwnerSpec `json:",inline"`
// Adds the given subject as capsule user. When enabled this subject does not have to be
// mentioned in the CapsuleConfiguration as Capsule User. In almost all scenarios Tenant Owners
// must be Capsule Users.
//+kubebuilder:default:=true
Aggregate bool `json:"aggregate"`
}
// TenantOwnerStatus defines the observed state of TenantOwner.

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package v1beta2

View File

@@ -10,7 +10,7 @@ package v1beta2
import (
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/misc"
"github.com/projectcapsule/capsule/pkg/runtime/selectors"
corev1 "k8s.io/api/core/v1"
"k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -43,6 +43,7 @@ func (in *CapsuleConfiguration) DeepCopyInto(out *CapsuleConfiguration) {
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CapsuleConfiguration.
@@ -129,6 +130,13 @@ func (in *CapsuleConfigurationSpec) DeepCopyInto(out *CapsuleConfigurationSpec)
*out = make(api.UserListSpec, len(*in))
copy(*out, *in)
}
in.Admission.DeepCopyInto(&out.Admission)
if in.RBAC != nil {
in, out := &in.RBAC, &out.RBAC
*out = new(RBACConfiguration)
(*in).DeepCopyInto(*out)
}
out.CacheInvalidation = in.CacheInvalidation
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CapsuleConfigurationSpec.
@@ -141,6 +149,27 @@ func (in *CapsuleConfigurationSpec) DeepCopy() *CapsuleConfigurationSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CapsuleConfigurationStatus) DeepCopyInto(out *CapsuleConfigurationStatus) {
*out = *in
in.LastCacheInvalidation.DeepCopyInto(&out.LastCacheInvalidation)
if in.Users != nil {
in, out := &in.Users, &out.Users
*out = make(api.UserListSpec, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CapsuleConfigurationStatus.
func (in *CapsuleConfigurationStatus) DeepCopy() *CapsuleConfigurationStatus {
if in == nil {
return nil
}
out := new(CapsuleConfigurationStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CapsuleResources) DeepCopyInto(out *CapsuleResources) {
*out = *in
@@ -156,6 +185,53 @@ func (in *CapsuleResources) DeepCopy() *CapsuleResources {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DynamicAdmission) DeepCopyInto(out *DynamicAdmission) {
*out = *in
in.Mutating.DeepCopyInto(&out.Mutating)
in.Validating.DeepCopyInto(&out.Validating)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynamicAdmission.
func (in *DynamicAdmission) DeepCopy() *DynamicAdmission {
if in == nil {
return nil
}
out := new(DynamicAdmission)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DynamicAdmissionConfig) DeepCopyInto(out *DynamicAdmissionConfig) {
*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
}
}
in.Client.DeepCopyInto(&out.Client)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynamicAdmissionConfig.
func (in *DynamicAdmissionConfig) DeepCopy() *DynamicAdmissionConfig {
if in == nil {
return nil
}
out := new(DynamicAdmissionConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GatewayOptions) DeepCopyInto(out *GatewayOptions) {
*out = *in
@@ -322,6 +398,11 @@ func (in *NamespaceOptions) DeepCopyInto(out *NamespaceOptions) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.RequiredMetadata != nil {
in, out := &in.RequiredMetadata, &out.RequiredMetadata
*out = new(RequiredMetadata)
(*in).DeepCopyInto(*out)
}
in.ForbiddenLabels.DeepCopyInto(&out.ForbiddenLabels)
in.ForbiddenAnnotations.DeepCopyInto(&out.ForbiddenAnnotations)
}
@@ -336,6 +417,65 @@ func (in *NamespaceOptions) DeepCopy() *NamespaceOptions {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRule) DeepCopyInto(out *NamespaceRule) {
*out = *in
in.NamespaceRuleBody.DeepCopyInto(&out.NamespaceRuleBody)
if in.NamespaceSelector != nil {
in, out := &in.NamespaceSelector, &out.NamespaceSelector
*out = new(metav1.LabelSelector)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRule.
func (in *NamespaceRule) DeepCopy() *NamespaceRule {
if in == nil {
return nil
}
out := new(NamespaceRule)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRuleBody) DeepCopyInto(out *NamespaceRuleBody) {
*out = *in
in.Enforce.DeepCopyInto(&out.Enforce)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRuleBody.
func (in *NamespaceRuleBody) DeepCopy() *NamespaceRuleBody {
if in == nil {
return nil
}
out := new(NamespaceRuleBody)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRuleEnforceBody) DeepCopyInto(out *NamespaceRuleEnforceBody) {
*out = *in
if in.Registries != nil {
in, out := &in.Registries, &out.Registries
*out = make([]api.OCIRegistry, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRuleEnforceBody.
func (in *NamespaceRuleEnforceBody) DeepCopy() *NamespaceRuleEnforceBody {
if in == nil {
return nil
}
out := new(NamespaceRuleEnforceBody)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NodeMetadata) DeepCopyInto(out *NodeMetadata) {
*out = *in
@@ -461,6 +601,31 @@ func (in ProcessedItems) DeepCopy() ProcessedItems {
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RBACConfiguration) DeepCopyInto(out *RBACConfiguration) {
*out = *in
if in.AdministrationClusterRoles != nil {
in, out := &in.AdministrationClusterRoles, &out.AdministrationClusterRoles
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.PromotionClusterRoles != nil {
in, out := &in.PromotionClusterRoles, &out.PromotionClusterRoles
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RBACConfiguration.
func (in *RBACConfiguration) DeepCopy() *RBACConfiguration {
if in == nil {
return nil
}
out := new(RBACConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RawExtension) DeepCopyInto(out *RawExtension) {
*out = *in
@@ -477,6 +642,35 @@ func (in *RawExtension) DeepCopy() *RawExtension {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RequiredMetadata) DeepCopyInto(out *RequiredMetadata) {
*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 RequiredMetadata.
func (in *RequiredMetadata) DeepCopy() *RequiredMetadata {
if in == nil {
return nil
}
out := new(RequiredMetadata)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResourcePool) DeepCopyInto(out *ResourcePool) {
*out = *in
@@ -590,6 +784,14 @@ func (in *ResourcePoolClaimStatus) DeepCopyInto(out *ResourcePoolClaimStatus) {
*out = *in
out.Pool = in.Pool
in.Condition.DeepCopyInto(&out.Condition)
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])
}
}
in.Allocation.DeepCopyInto(&out.Allocation)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourcePoolClaimStatus.
@@ -605,7 +807,7 @@ func (in *ResourcePoolClaimStatus) DeepCopy() *ResourcePoolClaimStatus {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResourcePoolClaimsItem) DeepCopyInto(out *ResourcePoolClaimsItem) {
*out = *in
out.StatusNameUID = in.StatusNameUID
out.NamespacedRFC1123ObjectReferenceWithNamespaceWithUID = in.NamespacedRFC1123ObjectReferenceWithNamespaceWithUID
if in.Claims != nil {
in, out := &in.Claims, &out.Claims
*out = make(corev1.ResourceList, len(*in))
@@ -759,7 +961,7 @@ func (in *ResourcePoolSpec) DeepCopyInto(out *ResourcePoolSpec) {
*out = *in
if in.Selectors != nil {
in, out := &in.Selectors, &out.Selectors
*out = make([]misc.NamespaceSelector, len(*in))
*out = make([]selectors.NamespaceSelector, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
@@ -853,6 +1055,13 @@ func (in *ResourcePoolStatus) DeepCopyInto(out *ResourcePoolStatus) {
(*out)[key] = *val.DeepCopy()
}
}
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 ResourcePoolStatus.
@@ -904,6 +1113,80 @@ func (in *ResourceSpec) DeepCopy() *ResourceSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RuleStatus) DeepCopyInto(out *RuleStatus) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuleStatus.
func (in *RuleStatus) DeepCopy() *RuleStatus {
if in == nil {
return nil
}
out := new(RuleStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *RuleStatus) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RuleStatusList) DeepCopyInto(out *RuleStatusList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]RuleStatus, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuleStatusList.
func (in *RuleStatusList) DeepCopy() *RuleStatusList {
if in == nil {
return nil
}
out := new(RuleStatusList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *RuleStatusList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RuleStatusSpec) DeepCopyInto(out *RuleStatusSpec) {
*out = *in
in.Rule.DeepCopyInto(&out.Rule)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuleStatusSpec.
func (in *RuleStatusSpec) DeepCopy() *RuleStatusSpec {
if in == nil {
return nil
}
out := new(RuleStatusSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Tenant) DeepCopyInto(out *Tenant) {
*out = *in
@@ -1220,6 +1503,17 @@ func (in *TenantResourceStatus) DeepCopy() *TenantResourceStatus {
func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
*out = *in
in.Permissions.DeepCopyInto(&out.Permissions)
if in.Rules != nil {
in, out := &in.Rules, &out.Rules
*out = make([]*NamespaceRule, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(NamespaceRule)
(*in).DeepCopyInto(*out)
}
}
}
if in.Owners != nil {
in, out := &in.Owners, &out.Owners
*out = make(api.OwnerListSpec, len(*in))
@@ -1248,11 +1542,6 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
(*in).DeepCopyInto(*out)
}
in.IngressOptions.DeepCopyInto(&out.IngressOptions)
if in.ContainerRegistries != nil {
in, out := &in.ContainerRegistries, &out.ContainerRegistries
*out = new(api.AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.NodeSelector != nil {
in, out := &in.NodeSelector, &out.NodeSelector
*out = make(map[string]string, len(*in))
@@ -1260,8 +1549,6 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
(*out)[key] = val
}
}
in.NetworkPolicies.DeepCopyInto(&out.NetworkPolicies)
in.LimitRanges.DeepCopyInto(&out.LimitRanges)
in.ResourceQuota.DeepCopyInto(&out.ResourceQuota)
if in.AdditionalRoleBindings != nil {
in, out := &in.AdditionalRoleBindings, &out.AdditionalRoleBindings
@@ -1270,11 +1557,6 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ImagePullPolicies != nil {
in, out := &in.ImagePullPolicies, &out.ImagePullPolicies
*out = make([]api.ImagePullPolicySpec, len(*in))
copy(*out, *in)
}
if in.RuntimeClasses != nil {
in, out := &in.RuntimeClasses, &out.RuntimeClasses
*out = new(api.DefaultAllowedListSpec)
@@ -1296,6 +1578,18 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) {
*out = new(bool)
**out = **in
}
if in.ContainerRegistries != nil {
in, out := &in.ContainerRegistries, &out.ContainerRegistries
*out = new(api.AllowedListSpec)
(*in).DeepCopyInto(*out)
}
if in.ImagePullPolicies != nil {
in, out := &in.ImagePullPolicies, &out.ImagePullPolicies
*out = make([]api.ImagePullPolicySpec, len(*in))
copy(*out, *in)
}
in.NetworkPolicies.DeepCopyInto(&out.NetworkPolicies)
in.LimitRanges.DeepCopyInto(&out.LimitRanges)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantSpec.
@@ -1354,6 +1648,28 @@ func (in *TenantStatus) DeepCopy() *TenantStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TenantStatusNamespaceEnforcement) DeepCopyInto(out *TenantStatusNamespaceEnforcement) {
*out = *in
if in.Registries != nil {
in, out := &in.Registries, &out.Registries
*out = make([]api.OCIRegistry, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantStatusNamespaceEnforcement.
func (in *TenantStatusNamespaceEnforcement) DeepCopy() *TenantStatusNamespaceEnforcement {
if in == nil {
return nil
}
out := new(TenantStatusNamespaceEnforcement)
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
@@ -1369,6 +1685,7 @@ func (in *TenantStatusNamespaceItem) DeepCopyInto(out *TenantStatusNamespaceItem
*out = new(TenantStatusNamespaceMetadata)
(*in).DeepCopyInto(*out)
}
in.Enforce.DeepCopyInto(&out.Enforce)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantStatusNamespaceItem.

View File

@@ -1,8 +1,8 @@
apiVersion: v2
type: application
description: A Helm chart to deploy the Capsule Operator for easily implementing,
managing, and maintaining mutitenancy and access control in Kubernetes.
home: https://github.com/projectcapsule/capsule
managing, and maintaining multitenancy and access control in Kubernetes.
home: https://projectcapsule.dev/
icon: https://github.com/projectcapsule/capsule/raw/main/assets/logo/capsule_small.png
dependencies:
- name: capsule-proxy

View File

@@ -67,7 +67,7 @@ The following Values have changed key or Value:
|-----|------|---------|-------------|
| affinity | object | `{}` | Set affinity rules for the Capsule pod |
| certManager.additionalSANS | list | `[]` | Specify additional SANS to add to the certificate |
| certManager.generateCertificates | bool | `false` | Specifies whether capsule webhooks certificates should be generated using cert-manager |
| certManager.generateCertificates | bool | `true` | Specifies whether capsule webhooks certificates should be generated using cert-manager |
| customAnnotations | object | `{}` | Additional annotations which will be added to all resources created by Capsule helm chart |
| customLabels | object | `{}` | Additional labels which will be added to all resources created by Capsule helm chart |
| extraManifests | list | `[]` | Array of additional resources to be created alongside Capsule helm chart |
@@ -88,9 +88,10 @@ The following Values have changed key or Value:
| securityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"enabled":true,"readOnlyRootFilesystem":true}` | Set the securityContext for the Capsule container |
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account. |
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created. |
| serviceAccount.imagePullSecrets | list | `[]` | |
| serviceAccount.name | string | `""` | The name of the service account to use. If not set and `serviceAccount.create=true`, a name is generated using the fullname template |
| tls.create | bool | `true` | When cert-manager is disabled, Capsule will generate the TLS certificate for webhook and CRDs conversion. |
| tls.enableController | bool | `true` | Start the Capsule controller that injects the CA into mutating and validating webhooks, and CRD as well. |
| tls.create | bool | `false` | When cert-manager is disabled, Capsule will generate the TLS certificate for webhook and CRDs conversion. |
| tls.enableController | bool | `false` | Start the Capsule controller that injects the CA into mutating and validating webhooks, and CRD as well. |
| tls.name | string | `""` | Override name of the Capsule TLS Secret name when externally managed. |
| tolerations | list | `[]` | Set list of tolerations for the Capsule pod |
| topologySpreadConstraints | list | `[]` | Set topology spread constraints for the Capsule pod |
@@ -115,6 +116,7 @@ The following Values have changed key or Value:
| manager.options.administrators | list | `[]` | Define entities which can act as Administrators in the capsule construct These entities are automatically owners for all existing tenants. Meaning they can add namespaces to any tenant. However they must be specific by using the capsule label for interacting with namespaces. Because if that label is not defined, it's assumed that namespace interaction was not targeted towards a tenant and will therefor be ignored by capsule. May also be handy in GitOps scenarios where certain service accounts need to be able to manage namespaces for all tenants. |
| 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.cacheInvalidation | string | `"24h0m0s"` | Duration after which the in-memory cache is invalidated (based on usaage) and re-fetched from the API server |
| manager.options.capsuleConfiguration | string | `"default"` | Change the default name of the capsule configuration name |
| manager.options.capsuleUserGroups | list | `[]` | DEPRECATED: use users properties. Names of the users considered as Capsule users. |
| manager.options.createConfiguration | bool | `true` | Create Configuration |
@@ -125,6 +127,11 @@ The following Values have changed key or Value:
| manager.options.logLevel | string | `"info"` | Set the log verbosity of the capsule with a value from 1 to 5 |
| 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 |
| manager.options.rbac | object | `{"administrationClusterRoles":["capsule-namespace-deleter"],"deleter":"capsule-namespace-deleter","promotionClusterRoles":["capsule-namespace-provisioner","capsule-namespace-deleter"],"provisioner":"capsule-namespace-provisioner"}` | Managed RBAC configuration for the controller |
| manager.options.rbac.administrationClusterRoles | list | `["capsule-namespace-deleter"]` | The ClusterRoles applied for Administrators |
| manager.options.rbac.deleter | string | `"capsule-namespace-deleter"` | Name for the ClusterRole required to grant Namespace Deletion permissions. |
| manager.options.rbac.promotionClusterRoles | list | `["capsule-namespace-provisioner","capsule-namespace-deleter"]` | The ClusterRoles applied for ServiceAccounts which had owner Promotion |
| manager.options.rbac.provisioner | string | `"capsule-namespace-provisioner"` | Name for the ClusterRole required to grant Namespace Provision permissions. |
| manager.options.userNames | list | `[]` | DEPRECATED: use users properties. Names of the users considered as Capsule users. |
| manager.options.users | list | `[{"kind":"Group","name":"projectcapsule.dev"}]` | Define entities which are considered part of the Capsule construct. Users not mentioned here will be ignored by Capsule |
| manager.options.workers | int | `1` | Workers (MaxConcurrentReconciles) is the maximum number of concurrent Reconciles which can be run (ALPHA). |
@@ -151,6 +158,14 @@ The following Values have changed key or Value:
| 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 |
| monitoring.diagnostics.annotations | object | `{}` | Annotations for dashboard configmaps |
| monitoring.diagnostics.enabled | bool | `false` | Enable Diagnostic Dashboards to be deployed |
| monitoring.diagnostics.labels | object | `{}` | Labels for dashboard configmaps |
| monitoring.diagnostics.operator.allowCrossNamespaceImport | bool | `true` | Allow the Operator to match this resource with Grafanas outside the current namespace |
| monitoring.diagnostics.operator.enabled | bool | `false` | Enable Operator Resources (GrafanaDashboard) |
| monitoring.diagnostics.operator.folder | string | `""` | folder assignment for dashboard |
| monitoring.diagnostics.operator.instanceSelector | object | `{}` | Selects Grafana instances for import |
| monitoring.diagnostics.operator.resyncPeriod | string | `"10m"` | How often the resource is synced, defaults to 10m0s if not set |
| monitoring.serviceMonitor.annotations | object | `{}` | Assign additional Annotations |
| monitoring.serviceMonitor.enabled | bool | `false` | Enable ServiceMonitor |
| monitoring.serviceMonitor.endpoint.interval | string | `"15s"` | Set the scrape interval for the endpoint of the serviceMonitor |
@@ -166,6 +181,7 @@ The following Values have changed key or Value:
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| webhooks.annotations | object | `{}` | Additional Annotations for all webhooks |
| webhooks.exclusive | bool | `false` | When `crds.exclusive` is `true` the webhooks will be installed |
| webhooks.hooks.config.enabled | bool | `true` | Enable the Hook |
| webhooks.hooks.config.failurePolicy | string | `"Ignore"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
@@ -176,16 +192,16 @@ The following Values have changed key or Value:
| webhooks.hooks.config.reinvocationPolicy | string | `"Never"` | [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) |
| webhooks.hooks.cordoning.enabled | bool | `true` | Enable the Hook |
| webhooks.hooks.cordoning.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
| webhooks.hooks.cordoning.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.cordoning.matchConditions | list | `[{"expression":"!has(request.subResource) || request.subResource == \"\"","name":"ignore-subresources"},{"expression":"request.resource.resource != \"events\"","name":"ignore-events"}]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.cordoning.matchPolicy | string | `"Equivalent"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.cordoning.namespaceSelector | object | `{"matchExpressions":[{"key":"capsule.clastix.io/tenant","operator":"Exists"},{"key":"projectcapsule.dev/cordoned","operator":"Exists"}]}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
| webhooks.hooks.cordoning.namespaceSelector | object | `{"matchExpressions":[{"key":"capsule.clastix.io/tenant","operator":"Exists"},{"key":"projectcapsule.dev/cordoned","operator":"In","values":["true"]}]}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
| webhooks.hooks.cordoning.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
| webhooks.hooks.cordoning.rules | list | `[{"apiGroups":["*"],"apiVersions":["*"],"operations":["CREATE","UPDATE","DELETE"],"resources":["*"],"scope":"Namespaced"}]` | [Rules](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-rules) |
| webhooks.hooks.customresources.enabled | bool | `true` | Enable the Hook |
| webhooks.hooks.customresources.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
| webhooks.hooks.customresources.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.customresources.matchConditions | list | `[{"expression":"!has(request.subResource) || request.subResource == \"\"","name":"ignore-subresources"},{"expression":"request.resource.resource != \"events\"","name":"ignore-events"}]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.customresources.matchPolicy | string | `"Equivalent"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.customresources.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.customresources.namespaceSelector | object | `{"matchExpressions":[{"key":"capsule.clastix.io/tenant","operator":"Exists"},{"key":"projectcapsule.dev/custom-resources","operator":"Exists"}]}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
| webhooks.hooks.customresources.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
| webhooks.hooks.defaults.ingress | object | `{}` | Deprecated, use webhooks.hooks.ingresses instead |
| webhooks.hooks.defaults.pods | object | `{}` | Deprecated, use webhooks.hooks.pods instead |
@@ -210,6 +226,13 @@ The following Values have changed key or Value:
| webhooks.hooks.ingresses.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.ingresses.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
| webhooks.hooks.ingresses.reinvocationPolicy | string | `"Never"` | [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) |
| webhooks.hooks.managed.enabled | bool | `true` | Enable the Hook |
| webhooks.hooks.managed.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
| webhooks.hooks.managed.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.managed.matchPolicy | string | `"Exact"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.managed.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.managed.objectSelector | object | `{"matchExpressions":[{"key":"projectcapsule.dev/managed-by","operator":"Exists"}]}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
| webhooks.hooks.managed.rules | list | `[{"apiGroups":["*"],"apiVersions":["*"],"operations":["CREATE","UPDATE","DELETE"],"resources":["*"],"scope":"*"}]` | [Rules](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-rules) |
| webhooks.hooks.namespaceOwnerReference | object | `{}` | Deprecated, use webhooks.hooks.namespaces instead |
| webhooks.hooks.namespaces.enabled | bool | `true` | Enable the Hook |
| webhooks.hooks.namespaces.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
@@ -218,12 +241,6 @@ The following Values have changed key or Value:
| webhooks.hooks.namespaces.namespaceSelector | object | `{}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
| webhooks.hooks.namespaces.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
| webhooks.hooks.namespaces.reinvocationPolicy | string | `"Never"` | [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) |
| webhooks.hooks.networkpolicies.enabled | bool | `true` | Enable the Hook |
| webhooks.hooks.networkpolicies.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
| webhooks.hooks.networkpolicies.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| 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 | `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) |
@@ -270,18 +287,13 @@ The following Values have changed key or Value:
| webhooks.hooks.services.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
| webhooks.hooks.tenantLabel.enabled | bool | `true` | Enable the Hook |
| webhooks.hooks.tenantLabel.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
| webhooks.hooks.tenantLabel.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.tenantLabel.matchConditions | list | `[{"expression":"!has(request.subResource) || request.subResource == \"\"","name":"ignore-subresources"},{"expression":"request.resource.resource != \"events\"","name":"ignore-events"}]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.tenantLabel.matchPolicy | string | `"Equivalent"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.tenantLabel.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.tenantLabel.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
| webhooks.hooks.tenantLabel.reinvocationPolicy | string | `"Never"` | [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) |
| webhooks.hooks.tenantLabel.rules | list | `[{"apiGroups":["*"],"apiVersions":["*"],"operations":["CREATE","UPDATE"],"resources":["*"],"scope":"Namespaced"}]` | [Rules](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-rules) |
| webhooks.hooks.tenantResourceObjects.enabled | bool | `true` | Enable the Hook |
| webhooks.hooks.tenantResourceObjects.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
| webhooks.hooks.tenantResourceObjects.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.tenantResourceObjects.matchPolicy | string | `"Exact"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
| webhooks.hooks.tenantResourceObjects.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.tenantResourceObjects.objectSelector | object | `{"matchExpressions":[{"key":"capsule.clastix.io/tenant","operator":"Exists"}]}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
| webhooks.hooks.tenantResourceObjects | object | `{}` | Deprecated, use webhooks.hooks.managed instead |
| webhooks.hooks.tenants.enabled | bool | `true` | Enable the Hook |
| webhooks.hooks.tenants.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
| webhooks.hooks.tenants.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
@@ -289,6 +301,7 @@ The following Values have changed key or Value:
| 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.labels | object | `{}` | Additional Labels for all webhooks |
| 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

@@ -0,0 +1,4 @@
replicaCount: 2
manager:
extraArgs:
- "--enable-leader-election=true"

View File

@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.20.0
name: capsuleconfigurations.capsule.clastix.io
spec:
group: capsule.clastix.io
@@ -64,6 +64,195 @@ spec:
- name
type: object
type: array
admission:
description: Configuration for dynamic Validating and Mutating Admission
webhooks managed by Capsule.
properties:
mutating:
description: Configure dynamic Mutating Admission for Capsule
properties:
annotations:
additionalProperties:
type: string
description: Annotations added to the Admission Webhook
type: object
client:
description: From the upstram struct
properties:
caBundle:
description: |-
`caBundle` is a PEM encoded CA bundle which will be used to validate the webhook's server certificate.
If unspecified, system trust roots on the apiserver are used.
format: byte
type: string
service:
description: |-
`service` is a reference to the service for this webhook. Either
`service` or `url` must be specified.
If the webhook is running within the cluster, then you should use `service`.
properties:
name:
description: |-
`name` is the name of the service.
Required
type: string
namespace:
description: |-
`namespace` is the namespace of the service.
Required
type: string
path:
description: |-
`path` is an optional URL path which will be sent in any request to
this service.
type: string
port:
description: |-
If specified, the port on the service that hosting webhook.
Default to 443 for backward compatibility.
`port` should be a valid port number (1-65535, inclusive).
format: int32
type: integer
required:
- name
- namespace
type: object
url:
description: |-
`url` gives the location of the webhook, in standard URL form
(`scheme://host:port/path`). Exactly one of `url` or `service`
must be specified.
The `host` should not refer to a service running in the cluster; use
the `service` field instead. The host might be resolved via external
DNS in some apiservers (e.g., `kube-apiserver` cannot resolve
in-cluster DNS as that would be a layering violation). `host` may
also be an IP address.
Please note that using `localhost` or `127.0.0.1` as a `host` is
risky unless you take great care to run this webhook on all hosts
which run an apiserver which might need to make calls to this
webhook. Such installs are likely to be non-portable, i.e., not easy
to turn up in a new cluster.
The scheme must be "https"; the URL must begin with "https://".
A path is optional, and if present may be any string permissible in
a URL. You may use the path to pass an arbitrary string to the
webhook, for example, a cluster identifier.
Attempting to use a user or basic auth e.g. "user:password@" is not
allowed. Fragments ("#...") and query parameters ("?...") are not
allowed, either.
type: string
type: object
labels:
additionalProperties:
type: string
description: Labels added to the Admission Webhook
type: object
name:
description: Name the Admission Webhook
maxLength: 63
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
required:
- client
type: object
validating:
description: Configure dynamic Validating Admission for Capsule
properties:
annotations:
additionalProperties:
type: string
description: Annotations added to the Admission Webhook
type: object
client:
description: From the upstram struct
properties:
caBundle:
description: |-
`caBundle` is a PEM encoded CA bundle which will be used to validate the webhook's server certificate.
If unspecified, system trust roots on the apiserver are used.
format: byte
type: string
service:
description: |-
`service` is a reference to the service for this webhook. Either
`service` or `url` must be specified.
If the webhook is running within the cluster, then you should use `service`.
properties:
name:
description: |-
`name` is the name of the service.
Required
type: string
namespace:
description: |-
`namespace` is the namespace of the service.
Required
type: string
path:
description: |-
`path` is an optional URL path which will be sent in any request to
this service.
type: string
port:
description: |-
If specified, the port on the service that hosting webhook.
Default to 443 for backward compatibility.
`port` should be a valid port number (1-65535, inclusive).
format: int32
type: integer
required:
- name
- namespace
type: object
url:
description: |-
`url` gives the location of the webhook, in standard URL form
(`scheme://host:port/path`). Exactly one of `url` or `service`
must be specified.
The `host` should not refer to a service running in the cluster; use
the `service` field instead. The host might be resolved via external
DNS in some apiservers (e.g., `kube-apiserver` cannot resolve
in-cluster DNS as that would be a layering violation). `host` may
also be an IP address.
Please note that using `localhost` or `127.0.0.1` as a `host` is
risky unless you take great care to run this webhook on all hosts
which run an apiserver which might need to make calls to this
webhook. Such installs are likely to be non-portable, i.e., not easy
to turn up in a new cluster.
The scheme must be "https"; the URL must begin with "https://".
A path is optional, and if present may be any string permissible in
a URL. You may use the path to pass an arbitrary string to the
webhook, for example, a cluster identifier.
Attempting to use a user or basic auth e.g. "user:password@" is not
allowed. Fragments ("#...") and query parameters ("?...") are not
allowed, either.
type: string
type: object
labels:
additionalProperties:
type: string
description: Labels added to the Admission Webhook
type: object
name:
description: Name the Admission Webhook
maxLength: 63
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
required:
- client
type: object
type: object
allowServiceAccountPromotion:
default: false
description: |-
@@ -71,6 +260,11 @@ spec:
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
cacheInvalidation:
default: 24h
description: Define the period of time upon a cache invalidation is
executed for all caches.
type: string
enableTLSReconciler:
default: false
description: |-
@@ -152,9 +346,38 @@ spec:
description: Disallow creation of namespaces, whose name matches this
regexp
type: string
rbac:
default: {}
description: Define Properties for managed ClusterRoles by Capsule
properties:
administrationClusterRoles:
default:
- capsule-namespace-deleter
description: The ClusterRoles applied for Administrators
items:
type: string
type: array
deleter:
default: capsule-namespace-deleter
description: Name for the ClusterRole required to grant Namespace
Deletion permissions.
type: string
promotionClusterRoles:
default:
- capsule-namespace-provisioner
- capsule-namespace-deleter
description: The ClusterRoles applied for ServiceAccounts which
had owner Promotion
items:
type: string
type: array
provisioner:
default: capsule-namespace-provisioner
description: Name for the ClusterRole required to grant Namespace
Provision permissions.
type: string
type: object
userGroups:
default:
- capsule.clastix.io
description: |-
Deprecated: use users property instead (https://projectcapsule.dev/docs/operating/setup/configuration/#users)
@@ -193,10 +416,44 @@ spec:
type: object
type: array
required:
- cacheInvalidation
- enableTLSReconciler
- rbac
type: object
status:
description: CapsuleConfigurationStatus defines the Capsule configuration
status.
properties:
lastCacheInvalidation:
description: Last time all caches were invalided
format: date-time
type: string
users:
description: Users which are considered Capsule Users and are bound
to the Capsule Tenant construct.
items:
properties:
kind:
description: Kind of entity. Possible values are "User", "Group",
and "ServiceAccount"
enum:
- User
- Group
- ServiceAccount
type: string
name:
description: Name of the entity.
type: string
required:
- kind
- name
type: object
type: array
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.20.0
name: globaltenantresources.capsule.clastix.io
spec:
group: capsule.clastix.io

View File

@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.20.0
name: resourcepoolclaims.capsule.clastix.io
spec:
group: capsule.clastix.io
@@ -19,18 +19,22 @@ spec:
jsonPath: .status.pool.name
name: Pool
type: string
- description: Status for claim
jsonPath: .status.condition.type
name: Status
- description: Ready Status
jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- description: Reason for status
jsonPath: .status.condition.reason
name: Reason
type: string
- description: Condition Message
jsonPath: .status.condition.message
- description: Ready Message
jsonPath: .status.conditions[?(@.type=="Ready")].message
name: Message
type: string
- description: Bound Status
jsonPath: .status.conditions[?(@.type=="Bound")].status
name: Bound
type: string
- description: Bound Message
jsonPath: .status.conditions[?(@.type=="Bound")].message
name: Reason
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
@@ -80,8 +84,44 @@ spec:
status:
description: ResourceQuotaClaimStatus defines the observed state of ResourceQuotaClaim.
properties:
allocation:
description: Tracks the Usage from Claimed from this claim and available
resources
properties:
available:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: Used to track the usage of the resource in the pool
(diff hard - claimed). May be used for further automation
type: object
hard:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Hard is the set of enforced hard limits for each named resource.
More info: https://kubernetes.io/docs/concepts/policy/resource-quotas/
type: object
used:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: Used is the current observed total usage of the resource
in the namespace.
type: object
type: object
condition:
description: Condtion for this resource claim
description: 'Deprecated: Use Conditions'
properties:
lastTransitionTime:
description: |-
@@ -133,23 +173,80 @@ spec:
- status
- type
type: object
conditions:
description: Conditions for the resource claim
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
pool:
description: Reference to the GlobalQuota being claimed from
properties:
name:
description: Name
maxLength: 253
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
namespace:
description: Namespace
maxLength: 253
description: Name of the referent.
maxLength: 63
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
uid:
description: UID of the tracked Tenant to pin point tracking
type: string
required:
- name
- uid
type: object
required:
- conditions
type: object
required:
- spec

View File

@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.20.0
name: resourcepools.capsule.clastix.io
spec:
group: capsule.clastix.io
@@ -25,6 +25,14 @@ spec:
jsonPath: .status.namespaceCount
name: Namespaces
type: integer
- description: Reconcile Status
jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- description: Reconcile Message
jsonPath: .status.conditions[?(@.type=="Ready")].message
name: Status
type: string
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
@@ -66,13 +74,13 @@ spec:
defaultsZero:
default: false
description: With this option all resources which can be allocated
are set to 0 for the resourcequota defaults.
are set to 0 for the resourcequota defaults. (Default false)
type: boolean
deleteBoundResources:
default: false
description: |-
When a resourcepool is deleted, the resourceclaims bound to it are disassociated from the resourcepool but not deleted.
By Enabling this option, the resourceclaims will be deleted when the resourcepool is deleted, if they are in bound state.
By Enabling this option, the resourceclaims will be deleted when the resourcepool is deleted, if they are in bound state. (Default false)
type: boolean
orderedQueue:
default: false
@@ -82,7 +90,7 @@ spec:
but if a lower priority claim still has enough space in the available resources, it will be able to claim them. Eventough
it's priority was lower
Enabling this option respects to Order. Meaning the Creationtimestamp matters and if a resource is put into the queue, no
other claim can claim the same resources with lower priority.
other claim can claim the same resources with lower priority. (Default false)
type: boolean
type: object
defaults:
@@ -275,22 +283,83 @@ spec:
description: Claimed resources
type: object
name:
description: Name
maxLength: 253
description: Name of the referent.
maxLength: 63
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
namespace:
description: Namespace
description: Namespace of the referent.
maxLength: 253
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
uid:
description: UID of the tracked Tenant to pin point tracking
type: string
required:
- name
- namespace
- uid
type: object
type: array
description: Tracks the quotas for the Resource.
type: object
conditions:
description: Conditions for the resource claim
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
exhaustions:
additionalProperties:
properties:
@@ -320,6 +389,8 @@ spec:
items:
type: string
type: array
required:
- conditions
type: object
required:
- spec

View File

@@ -0,0 +1,94 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.20.0
name: rulestatuses.capsule.clastix.io
spec:
group: capsule.clastix.io
names:
kind: RuleStatus
listKind: RuleStatusList
plural: rulestatuses
singular: rulestatus
scope: Namespaced
versions:
- additionalPrinterColumns:
- description: Age
jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1beta2
schema:
openAPIV3Schema:
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
status:
description: RuleStatus contains the accumulated rules applying to namespace
it's deployed in.
properties:
rule:
description: Managed Enforcement properties per Namespace (aggregated
from rules)
properties:
enforce:
description: Enforcement Rules applied
properties:
registries:
description: |-
Define registries which are allowed to be used within this tenant
The rules are aggregated, since you can use Regular Expressions the match registry endpoints
items:
properties:
policy:
description: Allowed PullPolicy for the given registry.
Supplying no value allows all policies.
items:
description: PullPolicy describes a policy for if/when
to pull a container image
type: string
type: array
url:
description: OCI Registry endpoint, is treated as regular
expression.
type: string
validation:
default:
- pod/images
- pod/volumes
description: Requesting Resources
items:
enum:
- pod/images
- pod/volumes
type: string
type: array
required:
- url
type: object
type: array
type: object
type: object
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.20.0
name: tenantowners.capsule.clastix.io
spec:
group: capsule.clastix.io
@@ -39,6 +39,13 @@ spec:
spec:
description: spec defines the desired state of TenantOwner.
properties:
aggregate:
default: true
description: |-
Adds the given subject as capsule user. When enabled this subject does not have to be
mentioned in the CapsuleConfiguration as Capsule User. In almost all scenarios Tenant Owners
must be Capsule Users.
type: boolean
clusterRoles:
default:
- admin
@@ -59,6 +66,7 @@ spec:
description: Name of the entity.
type: string
required:
- aggregate
- kind
- name
type: object

View File

@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.20.0
name: tenantresources.capsule.clastix.io
spec:
group: capsule.clastix.io

View File

@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.20.0
name: tenants.capsule.clastix.io
spec:
group: capsule.clastix.io
@@ -1191,9 +1191,10 @@ spec:
type: object
type: array
containerRegistries:
description: Specifies the trusted Image Registries assigned to the
Tenant. Capsule assures that all Pods resources created in the Tenant
can use only one of the allowed trusted registries. Optional.
description: |-
Deprecated: Use Enforcement.Registries instead
Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
properties:
allowed:
description: Match exact elements which are allowed as class names
@@ -1346,9 +1347,10 @@ spec:
x-kubernetes-map-type: atomic
type: object
imagePullPolicies:
description: Specify the allowed values for the imagePullPolicies
option in Pod resources. Capsule assures that all Pod resources
created in the Tenant can use only one of the allowed policy. Optional.
description: |-
Deprecated: Use Enforcement.Registries instead
Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
items:
enum:
- Always
@@ -1673,6 +1675,20 @@ spec:
format: int32
minimum: 1
type: integer
requiredMetadata:
description: Required Metadata for namespace within this tenant
properties:
annotations:
additionalProperties:
type: string
description: Annotations that must be defined for each namespace
type: object
labels:
additionalProperties:
type: string
description: Labels that must be defined for each namespace
type: object
type: object
type: object
networkPolicies:
description: |-
@@ -2464,6 +2480,100 @@ spec:
- Namespace
type: string
type: object
rules:
description: |-
Specify enforcement specifications for the scope of the Tenant.
We are moving all configuration enforcement. per namespace into a rule construct.
It's currently not final.
Read More: https://projectcapsule.dev/docs/tenants/rules/
items:
properties:
enforce:
description: Enforcement Rules applied
properties:
registries:
description: |-
Define registries which are allowed to be used within this tenant
The rules are aggregated, since you can use Regular Expressions the match registry endpoints
items:
properties:
policy:
description: Allowed PullPolicy for the given registry.
Supplying no value allows all policies.
items:
description: PullPolicy describes a policy for if/when
to pull a container image
type: string
type: array
url:
description: OCI Registry endpoint, is treated as
regular expression.
type: string
validation:
default:
- pod/images
- pod/volumes
description: Requesting Resources
items:
enum:
- pod/images
- pod/volumes
type: string
type: array
required:
- url
type: object
type: array
type: object
namespaceSelector:
description: Select namespaces which are going to usese
properties:
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
type: object
type: array
runtimeClasses:
description: |-
Specifies the allowed RuntimeClasses assigned to the Tenant.
@@ -2854,6 +2964,41 @@ spec:
- type
type: object
type: array
enforce:
description: Managed Metadata
properties:
registry:
description: Registries which are allowed within this namespace
items:
properties:
policy:
description: Allowed PullPolicy for the given registry.
Supplying no value allows all policies.
items:
description: PullPolicy describes a policy for if/when
to pull a container image
type: string
type: array
url:
description: OCI Registry endpoint, is treated as
regular expression.
type: string
validation:
default:
- pod/images
- pod/volumes
description: Requesting Resources
items:
enum:
- pod/images
- pod/volumes
type: string
type: array
required:
- url
type: object
type: array
type: object
metadata:
description: Managed Metadata
properties:
@@ -2892,8 +3037,6 @@ spec:
- size
- state
type: object
required:
- spec
type: object
served: true
storage: true

File diff suppressed because it is too large Load Diff

View File

@@ -155,6 +155,24 @@ service:
{{- end }}
{{- end }}
{{/*
Capsule Webhook service (Without Path)
*/}}
{{- define "capsule.webhooks.serviceConfig" -}}
{{- include "capsule.webhooks.cabundle" $ | nindent 0 }}
{{- if $.Values.webhooks.service.url }}
url: {{ trimSuffix "/" $.Values.webhooks.service.url }}
{{- else }}
service:
name: {{ default (printf "%s-webhook-service" (include "capsule.fullname" $)) $.Values.webhooks.service.name }}
namespace: {{ default $.Release.Namespace $.Values.webhooks.service.namespace }}
port: {{ default 443 $.Values.webhooks.service.port }}
{{- end }}
{{- end }}
{{/*
Capsule Webhook endpoint CA Bundle
*/}}
@@ -180,3 +198,22 @@ caBundle: {{ $.Values.webhooks.service.caBundle -}}
{{- $joined := join "," $sizes -}}
{{- sha256sum $joined -}}
{{- end -}}
{{- define "admission.labels" -}}
{{- with $.Values.webhooks.labels }}
{{- toYaml . | nindent 0 }}
{{- end }}
{{- end }}
{{- define "admission.annotations" -}}
{{- if and ($.Values.certManager.generateCertificates) (not $.Values.webhooks.service.caBundle) }}
cert-manager.io/inject-ca-from: {{ $.Release.Namespace }}/{{ include "capsule.fullname" $ }}-webhook-cert
{{- end }}
{{- with $.Values.customAnnotations }}
{{- toYaml . | nindent 0 }}
{{- end }}
{{- with $.Values.webhooks.annotations }}
{{- toYaml . | nindent 0 }}
{{- end }}
{{- end }}

View File

@@ -14,6 +14,34 @@ metadata:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
cacheInvalidation: {{ .Values.manager.options.cacheInvalidation }}
rbac:
{{- toYaml .Values.manager.options.rbac | nindent 4 }}
admission:
validating:
name: "{{ include "capsule.fullname" . }}-dynamic"
client:
{{- include "capsule.webhooks.serviceConfig" $ | nindent 8 }}
{{- if (include "admission.labels" $) }}
labels:
{{- include "admission.labels" $ | nindent 8 }}
{{- end }}
{{- if (include "admission.annotations" $) }}
annotations:
{{- include "admission.annotations" $ | nindent 8 }}
{{- end }}
mutating:
name: "{{ include "capsule.fullname" . }}-dynamic"
client:
{{- include "capsule.webhooks.serviceConfig" $ | nindent 8 }}
{{- if (include "admission.labels" $) }}
labels:
{{- include "admission.labels" $ | nindent 8 }}
{{- end }}
{{- if (include "admission.annotations" $) }}
annotations:
{{- include "admission.annotations" $ | nindent 8 }}
{{- end }}
administrators:
{{- toYaml .Values.manager.options.administrators | nindent 4 }}
users:

View File

@@ -32,6 +32,7 @@ rules:
- globaltenantresources.capsule.clastix.io
- tenants.capsule.clastix.io
- tenantowners.capsule.clastix.io
- rulestatuses.capsule.clastix.io
verbs:
- create
- delete

View File

@@ -0,0 +1,51 @@
{{- if $.Values.monitoring.diagnostics.enabled }}
{{ range $path, $_ := .Files.Glob "dashboards/**-dashboard.json" }}
{{- with $ }}
{{- $content := (.Files.Get $path) }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "capsule.fullname" . }}-{{ $path | base | trimSuffix "-dashboard.json" | regexFind "[^_]+$" }}-dashboard
namespace: {{ default $.Release.Namespace $.Values.monitoring.diagnostics.namespace | quote }}
annotations:
{{- with $.Values.monitoring.diagnostics.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "capsule.labels" $ | nindent 4 }}
{{- with $.Values.monitoring.diagnostics.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
data:
{{ base $path }}: |-
{{- $content | nindent 4 }}
{{- if $.Values.monitoring.diagnostics.operator.enabled }}
---
apiVersion: grafana.integreatly.org/v1beta1
kind: GrafanaDashboard
metadata:
name: {{ include "capsule.fullname" $ }}-{{ $path | base | trimSuffix "-dashboard.json" | regexFind "[^_]+$" }}
namespace: {{ default $.Release.Namespace $.Values.monitoring.diagnostics.namespace | quote }}
annotations:
{{- with $.Values.monitoring.diagnostics.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "capsule.labels" $ | nindent 4 }}
{{- with $.Values.monitoring.diagnostics.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
configMapRef:
name: {{ include "capsule.fullname" $ }}-{{ $path | base | trimSuffix "-dashboard.json" | regexFind "[^_]+$" }}-dashboard
key: {{ base $path }}
{{- with (omit $.Values.monitoring.diagnostics.operator "enabled") }}
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -3,15 +3,12 @@ apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: {{ include "capsule.fullname" . }}-mutating-webhook-configuration
namespace: {{ $.Release.Namespace }}
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- include "capsule.labels" $ | nindent 4 }}
{{- include "admission.labels" . | nindent 4 }}
annotations:
{{- if .Values.certManager.generateCertificates }}
cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "capsule.fullname" . }}-webhook-cert
{{- end }}
{{- with .Values.customAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- include "admission.annotations" . | nindent 4 }}
webhooks:
{{- with (mergeOverwrite .Values.webhooks.hooks.pods .Values.webhooks.hooks.defaults.pods) }}
{{- if .enabled }}
@@ -167,7 +164,7 @@ webhooks:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/namespace-patch" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/namespaces/mutating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
reinvocationPolicy: {{ .reinvocationPolicy }}
@@ -205,7 +202,7 @@ webhooks:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/resourcepool/mutating" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/resourcepools/mutating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
reinvocationPolicy: {{ .reinvocationPolicy }}
@@ -243,7 +240,7 @@ webhooks:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/resourcepool/claim/mutating" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/resourcepools/claim/mutating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
reinvocationPolicy: {{ .reinvocationPolicy }}
@@ -301,7 +298,7 @@ webhooks:
- apiGroups:
- capsule.clastix.io
apiVersions:
- v1beta2
- "*"
operations:
- CREATE
- UPDATE
@@ -315,7 +312,7 @@ webhooks:
{{- end }}
{{- with .Values.webhooks.hooks.tenantLabel }}
{{- if .enabled }}
- name: assignment.misc.projectcapsule.dev
- name: assign.misc.projectcapsule.dev
admissionReviewVersions:
- v1
- v1beta1

View File

@@ -11,5 +11,11 @@ metadata:
annotations:
{{- include "capsule.serviceAccountAnnotations" . | nindent 4 }}
{{- end }}
{{- with .Values.serviceAccount.imagePullSecrets }}
imagePullSecrets:
{{- range . }}
- name: {{ . | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -5,23 +5,57 @@ metadata:
name: {{ include "capsule.fullname" . }}-validating-webhook-configuration
namespace: {{ $.Release.Namespace }}
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- include "capsule.labels" $ | nindent 4 }}
{{- include "admission.labels" . | nindent 4 }}
annotations:
{{- if .Values.certManager.generateCertificates }}
cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "capsule.fullname" . }}-webhook-cert
{{- end }}
{{- with .Values.customAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- include "admission.annotations" . | nindent 4 }}
webhooks:
{{- with .Values.webhooks.hooks.cordoning }}
{{- with .Values.webhooks.hooks.customresources }}
{{- if .enabled }}
- name: cordoning.tenant.projectcapsule.dev
- name: customresources.misc.projectcapsule.dev
admissionReviewVersions:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/cordoning" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/misc/customresources" "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
- DELETE
resources:
- '*'
scope: Namespaced
sideEffects: None
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
{{- end }}
{{- end }}
{{- with .Values.webhooks.hooks.cordoning }}
{{- if .enabled }}
- name: cordoning.misc.projectcapsule.dev
admissionReviewVersions:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/misc/cordoning" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}
@@ -51,7 +85,7 @@ webhooks:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/devices" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/devices/validating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}
@@ -89,7 +123,7 @@ webhooks:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/gateways" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/gateways/validating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}
@@ -126,7 +160,7 @@ webhooks:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/ingresses" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/ingresses/validating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}
@@ -165,7 +199,7 @@ webhooks:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/namespaces" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/namespaces/validating " "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}
@@ -191,48 +225,13 @@ webhooks:
- DELETE
resources:
- namespaces
- namespaces/status
- namespace/finalize
scope: '*'
sideEffects: None
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
{{- end }}
{{- end }}
{{- with .Values.webhooks.hooks.networkpolicies }}
{{- if .enabled }}
- name: networkpolicies.projectcapsule.dev
admissionReviewVersions:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/networkpolicies" "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:
- networking.k8s.io
apiVersions:
- v1
operations:
- UPDATE
- DELETE
resources:
- networkpolicies
scope: Namespaced
sideEffects: None
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
{{- end }}
{{- end }}
{{- with .Values.webhooks.hooks.nodes }}
{{- if .enabled }}
- name: nodes.projectcapsule.dev
@@ -240,7 +239,7 @@ webhooks:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/nodes" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/nodes/validating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}
@@ -275,7 +274,7 @@ webhooks:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/pods" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/pods/validating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}
@@ -313,7 +312,7 @@ webhooks:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/persistentvolumeclaims" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/persistentvolumeclaims/validating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}
@@ -349,7 +348,7 @@ webhooks:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/services" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/services/validating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}
@@ -379,13 +378,13 @@ webhooks:
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
{{- end }}
{{- end }}
{{- with .Values.webhooks.hooks.tenantResourceObjects }}
{{- with (mergeOverwrite .Values.webhooks.hooks.managed .Values.webhooks.hooks.tenantResourceObjects) }}
{{- if .enabled }}
- name: resource-objects.tenant.projectcapsule.dev
admissionReviewVersions:
- v1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/tenantresource-objects" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/misc/managed" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}
@@ -401,16 +400,7 @@ webhooks:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
- apiGroups:
- '*'
apiVersions:
- '*'
operations:
- UPDATE
- DELETE
resources:
- '*'
scope: Namespaced
{{- toYaml .rules | nindent 4 }}
sideEffects: None
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
{{- end }}
@@ -460,7 +450,7 @@ webhooks:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/resourcepool/validating" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/resourcepools/validating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}
@@ -497,7 +487,7 @@ webhooks:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/resourcepool/claim/validating" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/resourcepools/claim/validating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}
@@ -528,52 +518,14 @@ webhooks:
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
{{- end }}
{{- end }}
{{- with .Values.webhooks.hooks.customresources }}
{{- if .enabled }}
- name: customresources.tenant.projectcapsule.dev
admissionReviewVersions:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/customresources" "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
- DELETE
resources:
- '*'
scope: Namespaced
sideEffects: None
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
{{- end }}
{{- end }}
{{- with .Values.webhooks.hooks.serviceaccounts }}
{{- if .enabled }}
- name: serviceaccounts.tenant.projectcapsule.dev
- name: serviceaccounts.projectcapsule.dev
admissionReviewVersions:
- v1
- v1beta1
clientConfig:
{{- include "capsule.webhooks.service" (dict "path" "/serviceaccounts" "ctx" $) | nindent 4 }}
{{- include "capsule.webhooks.service" (dict "path" "/serviceaccounts/validating" "ctx" $) | nindent 4 }}
failurePolicy: {{ .failurePolicy }}
matchPolicy: {{ .matchPolicy }}
{{- with .namespaceSelector }}

View File

@@ -331,6 +331,10 @@
"description": "Additional annotations to add to the CapsuleConfiguration resource",
"type": "object"
},
"cacheInvalidation": {
"description": "Duration after which the in-memory cache is invalidated (based on usaage) and re-fetched from the API server",
"type": "string"
},
"capsuleConfiguration": {
"description": "Change the default name of the capsule configuration name",
"type": "string"
@@ -395,6 +399,34 @@
"description": "If specified, disallows creation of namespaces matching the passed regexp",
"type": "string"
},
"rbac": {
"description": "Managed RBAC configuration for the controller",
"type": "object",
"properties": {
"administrationClusterRoles": {
"description": "The ClusterRoles applied for Administrators",
"type": "array",
"items": {
"type": "string"
}
},
"deleter": {
"description": "Name for the ClusterRole required to grant Namespace Deletion permissions.",
"type": "string"
},
"promotionClusterRoles": {
"description": "The ClusterRoles applied for ServiceAccounts which had owner Promotion",
"type": "array",
"items": {
"type": "string"
}
},
"provisioner": {
"description": "Name for the ClusterRole required to grant Namespace Provision permissions.",
"type": "string"
}
}
},
"userNames": {
"description": "DEPRECATED: use users properties. Names of the users considered as Capsule users.",
"type": "array"
@@ -525,6 +557,48 @@
}
}
},
"diagnostics": {
"type": "object",
"properties": {
"annotations": {
"description": "Annotations for dashboard configmaps",
"type": "object"
},
"enabled": {
"description": "Enable Diagnostic Dashboards to be deployed",
"type": "boolean"
},
"labels": {
"description": "Labels for dashboard configmaps",
"type": "object"
},
"operator": {
"type": "object",
"properties": {
"allowCrossNamespaceImport": {
"description": "Allow the Operator to match this resource with Grafanas outside the current namespace",
"type": "boolean"
},
"enabled": {
"description": "Enable Operator Resources (GrafanaDashboard)",
"type": "boolean"
},
"folder": {
"description": "folder assignment for dashboard",
"type": "string"
},
"instanceSelector": {
"description": "Selects Grafana instances for import",
"type": "object"
},
"resyncPeriod": {
"description": "How often the resource is synced, defaults to 10m0s if not set",
"type": "string"
}
}
}
}
},
"serviceMonitor": {
"type": "object",
"properties": {
@@ -710,6 +784,9 @@
"description": "Specifies whether a service account should be created.",
"type": "boolean"
},
"imagePullSecrets": {
"type": "array"
},
"name": {
"description": "The name of the service account to use. If not set and `serviceAccount.create=true`, a name is generated using the fullname template",
"type": "string"
@@ -744,6 +821,10 @@
"webhooks": {
"type": "object",
"properties": {
"annotations": {
"description": "Additional Annotations for all webhooks",
"type": "object"
},
"exclusive": {
"description": "When `crds.exclusive` is `true` the webhooks will be installed",
"type": "boolean"
@@ -797,7 +878,18 @@
},
"matchConditions": {
"description": "[MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)",
"type": "array"
"type": "array",
"items": {
"type": "object",
"properties": {
"expression": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"matchPolicy": {
"description": "[MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)",
@@ -817,6 +909,12 @@
},
"operator": {
"type": "string"
},
"values": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
@@ -878,7 +976,18 @@
},
"matchConditions": {
"description": "[MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)",
"type": "array"
"type": "array",
"items": {
"type": "object",
"properties": {
"expression": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"matchPolicy": {
"description": "[MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)",
@@ -1070,44 +1179,7 @@
}
}
},
"namespaceOwnerReference": {
"description": "Deprecated, use webhooks.hooks.namespaces instead",
"type": "object"
},
"namespaces": {
"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"
},
"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"
}
}
},
"networkpolicies": {
"managed": {
"type": "object",
"properties": {
"enabled": {
@@ -1148,7 +1220,96 @@
},
"objectSelector": {
"description": "[ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector)",
"type": "object",
"properties": {
"matchExpressions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"operator": {
"type": "string"
}
}
}
}
}
},
"rules": {
"description": "[Rules](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-rules)",
"type": "array",
"items": {
"type": "object",
"properties": {
"apiGroups": {
"type": "array",
"items": {
"type": "string"
}
},
"apiVersions": {
"type": "array",
"items": {
"type": "string"
}
},
"operations": {
"type": "array",
"items": {
"type": "string"
}
},
"resources": {
"type": "array",
"items": {
"type": "string"
}
},
"scope": {
"type": "string"
}
}
}
}
}
},
"namespaceOwnerReference": {
"description": "Deprecated, use webhooks.hooks.namespaces instead",
"type": "object"
},
"namespaces": {
"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"
},
"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"
}
}
},
@@ -1445,7 +1606,18 @@
},
"matchConditions": {
"description": "[MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)",
"type": "array"
"type": "array",
"items": {
"type": "object",
"properties": {
"expression": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"matchPolicy": {
"description": "[MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)",
@@ -1518,65 +1690,8 @@
}
},
"tenantResourceObjects": {
"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",
"properties": {
"matchExpressions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"operator": {
"type": "string"
}
}
}
}
}
}
}
"description": "Deprecated, use webhooks.hooks.managed instead",
"type": "object"
},
"tenants": {
"type": "object",
@@ -1613,6 +1728,10 @@
}
}
},
"labels": {
"description": "Additional Labels for all webhooks",
"type": "object"
},
"mutatingWebhooksTimeoutSeconds": {
"description": "Timeout in seconds for mutating webhooks",
"type": "integer"

View File

@@ -83,9 +83,9 @@ crds:
# Secret Options
tls:
# -- Start the Capsule controller that injects the CA into mutating and validating webhooks, and CRD as well.
enableController: true
enableController: false
# -- When cert-manager is disabled, Capsule will generate the TLS certificate for webhook and CRDs conversion.
create: true
create: false
# -- Override name of the Capsule TLS Secret name when externally managed.
name: ""
@@ -211,6 +211,25 @@ manager:
forbiddenAnnotations:
denied: []
deniedRegex: ""
# -- Duration after which the in-memory cache is invalidated (based on usaage) and re-fetched from the API server
cacheInvalidation: 24h0m0s
# -- Managed RBAC configuration for the controller
rbac:
# -- The ClusterRoles applied for Administrators
administrationClusterRoles:
- capsule-namespace-deleter
# -- The ClusterRoles applied for ServiceAccounts which had owner Promotion
promotionClusterRoles:
- capsule-namespace-provisioner
- capsule-namespace-deleter
# -- Name for the ClusterRole required to grant Namespace Deletion permissions.
deleter: capsule-namespace-deleter
# -- Name for the ClusterRole required to grant Namespace Provision permissions.
provisioner: capsule-namespace-provisioner
# -- DEPRECATED: use users properties.
# Names of the users considered as Capsule users.
userNames: []
@@ -218,7 +237,6 @@ manager:
# Names of the users considered as Capsule users.
capsuleUserGroups: []
# -- A list of extra arguments for the capsule controller
extraArgs:
- "--enable-leader-election=true"
@@ -318,9 +336,12 @@ serviceAccount:
# -- The name of the service account to use. If not set and `serviceAccount.create=true`, a name is generated using the fullname template
name: ""
imagePullSecrets: []
# just string array [ "sec-1", "sec-2" ]
certManager:
# -- Specifies whether capsule webhooks certificates should be generated using cert-manager
generateCertificates: false
generateCertificates: true
# -- Specify additional SANS to add to the certificate
additionalSANS: []
# -- Additional labels which will be added to all resources created by Capsule helm chart
@@ -341,6 +362,28 @@ extraManifests: []
# Monitoring Settings
monitoring:
diagnostics:
# -- Enable Diagnostic Dashboards to be deployed
enabled: false
# -- Annotations for dashboard configmaps
annotations: {}
# -- Labels for dashboard configmaps
labels: {}
# grafana_dashboard: "1"
# Grafana Operator
operator:
# -- Enable Operator Resources (GrafanaDashboard)
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
resyncPeriod: "10m"
# -- Selects Grafana instances for import
instanceSelector: {}
# -- folder assignment for dashboard
folder: ""
dashboards:
# -- Enable Dashboards to be deployed
enabled: false
@@ -351,6 +394,7 @@ monitoring:
# grafana_dashboard: "1"
# -- Custom namespace for dashboard configmaps
namespace: ""
# Grafana Operator
operator:
# -- Enable Operator Resources (GrafanaDashboard)
@@ -398,6 +442,12 @@ webhooks:
# -- Timeout in seconds for validating webhooks
validatingWebhooksTimeoutSeconds: 30
# -- Additional Labels for all webhooks
labels: {}
# -- Additional Annotations for all webhooks
annotations: {}
# Configure custom webhook service
service:
# -- The URL where the capsule webhook services are running (Overwrites cluster scoped service definition)
@@ -428,7 +478,12 @@ webhooks:
- key: capsule.clastix.io/tenant
operator: Exists
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
matchConditions: []
matchConditions:
- name: ignore-subresources
expression: '!has(request.subResource) || request.subResource == ""'
- name: ignore-events
expression: 'request.resource.resource != "events"'
# -- [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)
reinvocationPolicy: Never
# -- [Rules](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-rules)
@@ -487,8 +542,16 @@ webhooks:
matchExpressions:
- key: capsule.clastix.io/tenant
operator: Exists
- key: projectcapsule.dev/custom-resources
operator: Exists
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
matchConditions: []
matchConditions:
- name: ignore-subresources
expression: '!has(request.subResource) || request.subResource == ""'
- name: ignore-events
expression: 'request.resource.resource != "events"'
namespaces:
# -- Enable the Hook
@@ -521,9 +584,15 @@ webhooks:
- key: capsule.clastix.io/tenant
operator: Exists
- key: projectcapsule.dev/cordoned
operator: Exists
operator: In
values:
- "true"
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
matchConditions: []
matchConditions:
- name: ignore-subresources
expression: '!has(request.subResource) || request.subResource == ""'
- name: ignore-events
expression: 'request.resource.resource != "events"'
# -- [Rules](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-rules)
rules:
- apiGroups:
@@ -591,22 +660,6 @@ webhooks:
matchConditions: []
# -- [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)
reinvocationPolicy: Never
networkpolicies:
# -- 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: Equivalent
# -- [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: []
pods:
# -- Enable the Hook
@@ -678,7 +731,7 @@ webhooks:
# -- [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)
reinvocationPolicy: Never
tenantResourceObjects:
managed:
# -- Enable the Hook
enabled: true
# -- [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy)
@@ -688,7 +741,7 @@ webhooks:
# -- [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector)
objectSelector:
matchExpressions:
- key: capsule.clastix.io/tenant
- key: "projectcapsule.dev/managed-by"
operator: Exists
# -- [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector)
namespaceSelector:
@@ -697,6 +750,19 @@ webhooks:
operator: Exists
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
matchConditions: []
# -- [Rules](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-rules)
rules:
- apiGroups:
- '*'
apiVersions:
- '*'
operations:
- CREATE
- UPDATE
- DELETE
resources:
- '*'
scope: '*'
services:
# -- Enable the Hook
@@ -756,3 +822,6 @@ webhooks:
pvc: {}
# -- Deprecated, use webhooks.hooks.pods instead
pods: {}
# -- Deprecated, use webhooks.hooks.managed instead
tenantResourceObjects: {}

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package main
@@ -31,6 +31,8 @@ import (
capsulev1beta1 "github.com/projectcapsule/capsule/api/v1beta1"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/internal/cache"
"github.com/projectcapsule/capsule/internal/controllers/admission"
configcontroller "github.com/projectcapsule/capsule/internal/controllers/cfg"
podlabelscontroller "github.com/projectcapsule/capsule/internal/controllers/pod"
"github.com/projectcapsule/capsule/internal/controllers/pv"
@@ -51,7 +53,6 @@ import (
"github.com/projectcapsule/capsule/internal/webhook/misc"
namespacemutation "github.com/projectcapsule/capsule/internal/webhook/namespace/mutation"
namespacevalidation "github.com/projectcapsule/capsule/internal/webhook/namespace/validation"
"github.com/projectcapsule/capsule/internal/webhook/networkpolicy"
"github.com/projectcapsule/capsule/internal/webhook/node"
"github.com/projectcapsule/capsule/internal/webhook/pod"
"github.com/projectcapsule/capsule/internal/webhook/pvc"
@@ -63,8 +64,9 @@ import (
tenantvalidation "github.com/projectcapsule/capsule/internal/webhook/tenant/validation"
tntresource "github.com/projectcapsule/capsule/internal/webhook/tenantresource"
"github.com/projectcapsule/capsule/internal/webhook/utils"
"github.com/projectcapsule/capsule/pkg/configuration"
"github.com/projectcapsule/capsule/pkg/indexer"
"github.com/projectcapsule/capsule/pkg/runtime/configuration"
"github.com/projectcapsule/capsule/pkg/runtime/handlers"
"github.com/projectcapsule/capsule/pkg/runtime/indexers"
)
var (
@@ -190,7 +192,7 @@ func main() {
if directCfg.EnableTLSConfiguration() {
tlsReconciler := &tlscontroller.Reconciler{
Client: directClient,
Log: ctrl.Log.WithName("controllers").WithName("TLS"),
Log: ctrl.Log.WithName("capsule.ctrl").WithName("tls"),
Namespace: ns,
Configuration: directCfg,
}
@@ -213,12 +215,14 @@ func main() {
}
}
registryCache := cache.NewRegistryRuleSetCache()
if err = (&tenantcontroller.Manager{
RESTConfig: manager.GetConfig(),
Client: manager.GetClient(),
Metrics: metrics.MustMakeTenantRecorder(),
Log: ctrl.Log.WithName("controllers").WithName("Tenant"),
Recorder: manager.GetEventRecorderFor("tenant-controller"),
Log: ctrl.Log.WithName("capsule.ctrl").WithName("tenant"),
Recorder: manager.GetEventRecorder("tenant-controller"),
Configuration: cfg,
}).SetupWithManager(manager, controllerConfig); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Tenant")
@@ -230,7 +234,7 @@ func main() {
os.Exit(1)
}
if err = indexer.AddToManager(ctx, setupLog, manager); err != nil {
if err = indexers.AddToManager(ctx, setupLog, manager); err != nil {
setupLog.Error(err, "unable to setup indexers")
os.Exit(1)
}
@@ -244,11 +248,12 @@ func main() {
// webhooks: the order matters, don't change it and just append
webhooksList := append(
make([]webhook.Webhook, 0),
make([]handlers.Webhook, 0),
route.Pod(
pod.Handler(
pod.ImagePullPolicy(),
pod.ContainerRegistry(cfg),
pod.ContainerRegistryLegacy(cfg),
pod.ContainerRegistry(cfg, registryCache),
pod.PriorityClass(),
pod.RuntimeClass(),
),
@@ -265,16 +270,15 @@ func main() {
service.Validating(),
),
),
route.TenantResourceObjects(utils.InCapsuleGroups(cfg, tntresource.WriteOpsHandler())),
route.NetworkPolicy(utils.InCapsuleGroups(cfg, networkpolicy.Handler())),
route.Cordoning(tenantvalidation.CordoningHandler(cfg)),
route.Node(utils.InCapsuleGroups(cfg, node.UserMetadataHandler(cfg, kubeVersion))),
route.TenantResourceObjects(handlers.InCapsuleGroups(cfg, tntresource.WriteOpsHandler())),
route.Cordoning(handlers.InCapsuleGroups(cfg, misc.CordoningHandler(cfg))),
route.Node(handlers.InCapsuleGroups(cfg, node.UserMetadataHandler(cfg, kubeVersion))),
route.ServiceAccounts(
serviceaccounts.Handler(
serviceaccounts.Validating(cfg),
),
),
route.CustomResources(tenantvalidation.ResourceCounterHandler(manager.GetClient())),
route.MiscCustomResources(misc.ResourceCounterHandler(manager.GetClient())),
route.Gateway(gateway.Class(cfg)),
route.DeviceClass(dra.DeviceClass()),
route.Defaults(defaults.Handler(cfg, kubeVersion)),
@@ -282,17 +286,21 @@ func main() {
tenantmutation.MetaHandler(),
),
route.TenantValidation(
tenantvalidation.NameHandler(),
tenantvalidation.RoleBindingRegexHandler(),
tenantvalidation.IngressClassRegexHandler(),
tenantvalidation.StorageClassRegexHandler(),
tenantvalidation.ContainerRegistryRegexHandler(),
tenantvalidation.HostnameRegexHandler(),
tenantvalidation.FreezedEmitter(),
tenantvalidation.ServiceAccountNameHandler(),
tenantvalidation.ForbiddenAnnotationsRegexHandler(),
tenantvalidation.ProtectedHandler(),
tenantvalidation.WarningHandler(cfg),
tenantvalidation.Handler(cfg,
tenantvalidation.NameHandler(),
tenantvalidation.RoleBindingRegexHandler(),
tenantvalidation.IngressClassRegexHandler(),
tenantvalidation.StorageClassRegexHandler(),
tenantvalidation.ContainerRegistryRegexHandler(),
tenantvalidation.RuleHandler(),
tenantvalidation.HostnameRegexHandler(),
tenantvalidation.FreezedEmitter(),
tenantvalidation.ServiceAccountNameHandler(),
tenantvalidation.ForbiddenAnnotationsRegexHandler(),
tenantvalidation.ProtectedHandler(),
tenantvalidation.RequiredMetadataHandler(),
tenantvalidation.WarningHandler(cfg),
),
),
route.NamespaceValidation(
namespacevalidation.NamespaceHandler(
@@ -302,6 +310,7 @@ func main() {
namespacevalidation.QuotaHandler(),
namespacevalidation.PrefixHandler(cfg),
namespacevalidation.UserMetadataHandler(),
namespacevalidation.RequiredMetadataHandler(),
),
),
route.NamespaceMutation(
@@ -316,9 +325,12 @@ func main() {
route.ResourcePoolValidation((resourcepool.PoolValidationHandler(ctrl.Log.WithName("webhooks").WithName("resourcepool")))),
route.ResourcePoolClaimMutation((resourcepool.ClaimMutationHandler(ctrl.Log.WithName("webhooks").WithName("resourcepoolclaims")))),
route.ResourcePoolClaimValidation((resourcepool.ClaimValidationHandler(ctrl.Log.WithName("webhooks").WithName("resourcepoolclaims")))),
route.TenantAssignment(
route.MiscTenantAssignment(
misc.TenantAssignmentHandler(),
),
route.MiscManagedValidation(
handlers.InCapsuleGroups(cfg, misc.ManagedValidatingHandler()),
),
route.ConfigValidation(
cfgvalidation.WarningHandler(),
),
@@ -326,7 +338,7 @@ func main() {
nodeWebhookSupported, _ := utils.NodeWebhookSupported(kubeVersion)
if !nodeWebhookSupported {
setupLog.Info("Disabling node labels verification webhook as current Kubernetes version doesn't have fix for CVE-2021-25735")
setupLog.Info("disabling node labels verification webhook as current Kubernetes version doesn't have fix for CVE-2021-25735")
}
if err = webhook.Register(manager, webhooksList...); err != nil {
@@ -335,7 +347,7 @@ func main() {
}
rbacManager := &rbaccontroller.Manager{
Log: ctrl.Log.WithName("controllers").WithName("Rbac"),
Log: ctrl.Log.WithName("capsule.ctrl").WithName("rbac"),
Client: manager.GetClient(),
Configuration: cfg,
}
@@ -351,19 +363,22 @@ func main() {
}
if err = (&servicelabelscontroller.ServicesLabelsReconciler{
Log: ctrl.Log.WithName("controllers").WithName("ServiceLabels"),
Log: ctrl.Log.WithName("capsule.ctrl").WithName("services"),
}).SetupWithManager(ctx, manager); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ServiceLabels")
os.Exit(1)
}
if err = (&servicelabelscontroller.EndpointSlicesLabelsReconciler{
Log: ctrl.Log.WithName("controllers").WithName("EndpointSliceLabels"),
Log: ctrl.Log.WithName("capsule.ctrl").WithName("endpointslices"),
}).SetupWithManager(ctx, manager); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "EndpointSliceLabels")
}
if err = (&podlabelscontroller.MetadataReconciler{Client: manager.GetClient()}).SetupWithManager(ctx, manager); err != nil {
if err = (&podlabelscontroller.MetadataReconciler{
Client: manager.GetClient(),
Log: ctrl.Log.WithName("capsule.ctrl").WithName("pods"),
}).SetupWithManager(ctx, manager); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "PodLabels")
os.Exit(1)
}
@@ -374,7 +389,9 @@ func main() {
}
if err = (&configcontroller.Manager{
Log: ctrl.Log.WithName("controllers").WithName("CapsuleConfiguration"),
Client: manager.GetClient(),
RegistryCache: registryCache,
Log: ctrl.Log.WithName("capsule.ctrl").WithName("configuration"),
}).SetupWithManager(manager, controllerConfig); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "CapsuleConfiguration")
os.Exit(1)
@@ -390,10 +407,21 @@ func main() {
os.Exit(1)
}
if err := resourcepools.Add(
ctrl.Log.WithName("controllers").WithName("ResourcePools"),
if err := admission.Add(
ctrl.Log.WithName("capsule.ctrl").WithName("admission"),
manager,
manager.GetEventRecorderFor("pools-ctrl"),
manager.GetEventRecorder("admission-ctrl"),
controllerConfig,
cfg,
); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "admission")
os.Exit(1)
}
if err := resourcepools.Add(
ctrl.Log.WithName("capsule.ctrl").WithName("resourcepools"),
manager,
manager.GetEventRecorder("pools-ctrl"),
controllerConfig,
); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "resourcepools")

View File

@@ -1,4 +1,4 @@
// Copyright 2020-2025 Project Capsule Authors
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package main

View File

@@ -5,12 +5,12 @@ package e2e
import (
"context"
"fmt"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
rbacv1 "k8s.io/api/rbac/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/api"
@@ -58,20 +58,10 @@ var _ = Describe("creating a Namespace with an additional Role Binding", Label("
})
It("should be assigned to each Namespace", func() {
for _, ns := range []string{"rb-1", "rb-2", "rb-3"} {
ns := NewNamespace(ns)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
var rb *rbacv1.RoleBinding
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
Eventually(func() (err error) {
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
rb, err = cs.RbacV1().RoleBindings(ns.Name).Get(context.Background(), fmt.Sprintf("capsule-%s-2-%s", tnt.Name, "crds-rolebinding"), metav1.GetOptions{})
return err
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
Expect(rb.RoleRef.Name).Should(Equal(tnt.Spec.AdditionalRoleBindings[0].ClusterRoleName))
Expect(rb.Subjects).Should(Equal(tnt.Spec.AdditionalRoleBindings[0].Subjects))
}
VerifyTenantRoleBindings(t)
})
})

View File

@@ -393,6 +393,7 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
"matching_namespace_label": "matching_namespace_label_value",
"capsule.clastix.io/tenant": tnt.GetName(),
"kubernetes.io/metadata.name": ns.GetName(),
"env": "e2e",
}
Eventually(func() map[string]string {
@@ -490,6 +491,7 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
"matching_namespace_label": "matching_namespace_label_value",
"capsule.clastix.io/tenant": tnt.GetName(),
"kubernetes.io/metadata.name": ns.GetName(),
"env": "e2e",
}
Eventually(func() map[string]string {
got := &corev1.Namespace{}
@@ -579,6 +581,7 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
"matching_namespace_label": "matching_namespace_label_value",
"capsule.clastix.io/tenant": tnt.GetName(),
"kubernetes.io/metadata.name": ns.GetName(),
"env": "e2e",
}
Eventually(func() map[string]string {
got := &corev1.Namespace{}
@@ -660,6 +663,7 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
"matching_namespace_label": "matching_namespace_label_value",
"capsule.clastix.io/tenant": tnt.GetName(),
"kubernetes.io/metadata.name": ns.GetName(),
"env": "e2e",
}
Eventually(func() map[string]string {
got := &corev1.Namespace{}

View File

@@ -77,6 +77,7 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadata.Labels {
Expect(ns.Labels).To(HaveKeyWithValue(k, v))
}
return
})
By("checking additional annotations", func() {

View File

@@ -0,0 +1,126 @@
// 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/api"
)
var _ = Describe("creating a Namespace for a Tenant with required metadata", Label("namespace", "metadata", "me"), func() {
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "tenant-metadata-required",
},
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "gatsby",
Kind: "User",
},
},
},
},
NamespaceOptions: &capsulev1beta2.NamespaceOptions{
RequiredMetadata: &capsulev1beta2.RequiredMetadata{
Labels: map[string]string{
"environment": "^(prod|test|dev)$",
},
Annotations: map[string]string{
"example.corp/cost-center": "^INV-[0-9]{4}$",
},
},
},
},
}
JustBeforeEach(func() {
EventuallyCreation(func() error {
return k8sClient.Create(context.TODO(), tnt)
}).Should(Succeed())
})
JustAfterEach(func() {
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
})
It("should contain required Namespace metadata", func() {
By("creating without required label", func() {
ns := NewNamespace("")
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).ShouldNot(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).ShouldNot(ContainElement(ns.GetName()))
})
By("creating with required label, without annotation", func() {
ns := NewNamespace("", map[string]string{
"environment": "prod",
})
ns.SetAnnotations(map[string]string{})
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).ShouldNot(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).ShouldNot(ContainElement(ns.GetName()))
})
By("creating with required label and annotation", func() {
ns := NewNamespace("", map[string]string{
"environment": "prod",
})
ns.SetAnnotations(map[string]string{
"example.corp/cost-center": "INV-1234",
})
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
ns.SetLabels(map[string]string{
"environment": "UAT",
})
ns.SetAnnotations(map[string]string{
"example.corp/cost-center": "INV-1",
})
c := impersonationClient(tnt.Spec.Owners[0].UserSpec.Name, withDefaultGroups(nil))
err := c.Update(context.TODO(), ns)
Expect(err).ShouldNot(Succeed(), "expected failure")
})
By("creating with required label (wrong value) and annotation", func() {
ns := NewNamespace("", map[string]string{
"environment": "UAT",
})
ns.SetAnnotations(map[string]string{
"example.corp/cost-center": "INV-1234",
})
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).ShouldNot(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).ShouldNot(ContainElement(ns.GetName()))
})
By("creating with required label and annotation (wrong value)", func() {
ns := NewNamespace("", map[string]string{
"environment": "prod",
})
ns.SetAnnotations(map[string]string{
"example.corp/cost-center": "INV-1",
})
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).ShouldNot(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).ShouldNot(ContainElement(ns.GetName()))
})
})
})

View File

@@ -14,6 +14,7 @@ import (
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
)
var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
@@ -123,6 +124,7 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
},
},
Spec: capsulev1beta2.TenantOwnerSpec{
Aggregate: true,
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
@@ -143,6 +145,7 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
},
},
Spec: capsulev1beta2.TenantOwnerSpec{
Aggregate: true,
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.GroupOwner,
@@ -164,6 +167,7 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
},
},
Spec: capsulev1beta2.TenantOwnerSpec{
Aggregate: true,
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.ServiceAccountOwner,
@@ -176,6 +180,49 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
},
}
tnt1Owner := &capsulev1beta2.TenantOwner{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-owners-tnt",
Labels: map[string]string{
meta.NewTenantLabel: tnt1.GetName(),
},
},
Spec: capsulev1beta2.TenantOwnerSpec{
Aggregate: true,
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "tnt-1-user",
},
ClusterRoles: []string{
"service-admin",
},
},
},
}
userOwnersCommon := &capsulev1beta2.TenantOwner{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-owners-common-user",
Labels: map[string]string{
"team": "infrastructure",
"customer": "x",
},
},
Spec: capsulev1beta2.TenantOwnerSpec{
Aggregate: false,
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Kind: api.UserOwner,
Name: "some-user",
},
ClusterRoles: []string{
"service-admin",
},
},
},
}
JustBeforeEach(func() {
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: defaultConfigurationName}, originConfig)).To(Succeed())
@@ -187,7 +234,7 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
}).Should(Succeed())
}
for _, tnt := range []*capsulev1beta2.TenantOwner{ownersInfra, ownersDevops, ownersCommon} {
for _, tnt := range []*capsulev1beta2.TenantOwner{ownersInfra, ownersDevops, ownersCommon, userOwnersCommon, tnt1Owner} {
EventuallyCreation(func() error {
tnt.ResourceVersion = ""
@@ -202,13 +249,39 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
Expect(client.IgnoreNotFound(err)).To(Succeed())
}
for _, owners := range []*capsulev1beta2.TenantOwner{ownersInfra, ownersDevops, ownersCommon} {
for _, owners := range []*capsulev1beta2.TenantOwner{ownersInfra, ownersDevops, ownersCommon, userOwnersCommon, tnt1Owner} {
err := k8sClient.Delete(context.TODO(), owners)
Expect(client.IgnoreNotFound(err)).To(Succeed())
}
})
It("Verify owners for", func() {
By("checking configuration", func() {
Eventually(func(g Gomega) {
cfg := &capsulev1beta2.CapsuleConfiguration{}
err := k8sClient.Get(
context.Background(),
client.ObjectKey{Name: defaultConfigurationName},
cfg,
)
g.Expect(err).ToNot(HaveOccurred())
expected := api.UserListSpec{
{Kind: ownersInfra.Spec.Kind, Name: ownersInfra.Spec.Name},
{Kind: ownersDevops.Spec.Kind, Name: ownersDevops.Spec.Name},
{Kind: ownersCommon.Spec.Kind, Name: ownersCommon.Spec.Name},
{Kind: tnt1Owner.Spec.Kind, Name: tnt1Owner.Spec.Name},
{Kind: api.GroupOwner, Name: "projectcapsule.dev"},
}
g.Expect(cfg.Status.Users).To(ConsistOf(expected))
g.Expect(cfg.Status.Users).NotTo(ContainElement(
api.UserSpec{Kind: userOwnersCommon.Spec.Kind, Name: userOwnersCommon.Spec.Name},
))
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
By("checking owners (e2e-owners-1)", func() {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed())
@@ -242,6 +315,20 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: userOwnersCommon.Spec.Kind,
Name: userOwnersCommon.Spec.Name,
},
ClusterRoles: []string{"service-admin"},
},
{
UserSpec: api.UserSpec{
Kind: tnt1Owner.Spec.Kind,
Name: tnt1Owner.Spec.Name,
},
ClusterRoles: []string{"service-admin"},
},
}
Expect(normalizeOwners(t.Status.Owners)).
@@ -250,6 +337,41 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
VerifyTenantRoleBindings(t)
})
By("creating namespaces (e2e-owners-1)", func() {
for _, u := range []api.UserSpec{
api.UserSpec{
Kind: api.GroupOwner,
Name: "e2e-owners-1-group",
},
api.UserSpec{
Kind: api.GroupOwner,
Name: "oidc:comp:devops",
},
api.UserSpec{
Kind: api.ServiceAccountOwner,
Name: "system:serviceaccount:capsule-system:capsule",
},
api.UserSpec{
Kind: api.UserOwner,
Name: "e2e-owners-1",
},
api.UserSpec{
Kind: userOwnersCommon.Spec.Kind,
Name: userOwnersCommon.Spec.Name,
},
api.UserSpec{
Kind: tnt1Owner.Spec.Kind,
Name: tnt1Owner.Spec.Name,
},
} {
ns := NewNamespace("", map[string]string{
meta.TenantLabel: tnt1.GetName(),
})
NamespaceCreation(ns, u, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt1, defaultTimeoutInterval).Should(ContainElements(ns.GetName()))
}
})
By("checking owners (e2e-owners-2)", func() {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt2.GetName()}, t)).Should(Succeed())
@@ -283,6 +405,14 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: userOwnersCommon.Spec.Kind,
Name: userOwnersCommon.Spec.Name,
},
ClusterRoles: []string{"service-admin"},
},
}
Expect(normalizeOwners(t.Status.Owners)).
@@ -291,10 +421,62 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
VerifyTenantRoleBindings(t)
})
By("creating namespaces (e2e-owners-2)", func() {
for _, u := range []api.UserSpec{
api.UserSpec{
Kind: api.GroupOwner,
Name: "e2e-owners-2-group",
},
api.UserSpec{
Kind: api.GroupOwner,
Name: "oidc:comp:administrators",
},
api.UserSpec{
Kind: api.ServiceAccountOwner,
Name: "system:serviceaccount:capsule-system:capsule",
},
api.UserSpec{
Kind: api.UserOwner,
Name: "e2e-owners-2",
},
api.UserSpec{
Kind: userOwnersCommon.Spec.Kind,
Name: userOwnersCommon.Spec.Name,
},
} {
ns := NewNamespace("", map[string]string{
meta.TenantLabel: tnt2.GetName(),
})
NamespaceCreation(ns, u, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt2, defaultTimeoutInterval).Should(ContainElements(ns.GetName()))
}
})
By("remove common tenant-owners", func() {
Expect(k8sClient.Delete(context.TODO(), ownersCommon)).Should(Succeed())
})
By("checking configuration", func() {
Eventually(func(g Gomega) {
cfg := &capsulev1beta2.CapsuleConfiguration{}
err := k8sClient.Get(
context.Background(),
client.ObjectKey{Name: defaultConfigurationName},
cfg,
)
g.Expect(err).ToNot(HaveOccurred())
expected := api.UserListSpec{
{Kind: ownersInfra.Spec.Kind, Name: ownersInfra.Spec.Name},
{Kind: ownersDevops.Spec.Kind, Name: ownersDevops.Spec.Name},
{Kind: tnt1Owner.Spec.Kind, Name: tnt1Owner.Spec.Name},
{Kind: api.GroupOwner, Name: "projectcapsule.dev"},
}
g.Expect(cfg.Status.Users).To(ConsistOf(expected))
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
By("checking owners (e2e-owners-1)", func() {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed())
@@ -328,6 +510,20 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: userOwnersCommon.Spec.Kind,
Name: userOwnersCommon.Spec.Name,
},
ClusterRoles: []string{"service-admin"},
},
{
UserSpec: api.UserSpec{
Kind: tnt1Owner.Spec.Kind,
Name: tnt1Owner.Spec.Name,
},
ClusterRoles: []string{"service-admin"},
},
}
Expect(normalizeOwners(t.Status.Owners)).
@@ -369,6 +565,13 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: userOwnersCommon.Spec.Kind,
Name: userOwnersCommon.Spec.Name,
},
ClusterRoles: []string{"service-admin"},
},
}
Expect(normalizeOwners(t.Status.Owners)).
@@ -381,6 +584,26 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
Expect(k8sClient.Delete(context.TODO(), ownersInfra)).Should(Succeed())
})
By("checking configuration", func() {
Eventually(func(g Gomega) {
cfg := &capsulev1beta2.CapsuleConfiguration{}
err := k8sClient.Get(
context.Background(),
client.ObjectKey{Name: defaultConfigurationName},
cfg,
)
g.Expect(err).ToNot(HaveOccurred())
expected := api.UserListSpec{
{Kind: ownersDevops.Spec.Kind, Name: ownersDevops.Spec.Name},
{Kind: tnt1Owner.Spec.Kind, Name: tnt1Owner.Spec.Name},
{Kind: api.GroupOwner, Name: "projectcapsule.dev"},
}
g.Expect(cfg.Status.Users).To(ConsistOf(expected))
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
By("checking owners (e2e-owners-1)", func() {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed())
@@ -414,6 +637,20 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: userOwnersCommon.Spec.Kind,
Name: userOwnersCommon.Spec.Name,
},
ClusterRoles: []string{"service-admin"},
},
{
UserSpec: api.UserSpec{
Kind: tnt1Owner.Spec.Kind,
Name: tnt1Owner.Spec.Name,
},
ClusterRoles: []string{"service-admin"},
},
}
Expect(normalizeOwners(t.Status.Owners)).
@@ -448,6 +685,13 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: userOwnersCommon.Spec.Kind,
Name: userOwnersCommon.Spec.Name,
},
ClusterRoles: []string{"service-admin"},
},
}
Expect(normalizeOwners(t.Status.Owners)).
@@ -460,6 +704,25 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
Expect(k8sClient.Delete(context.TODO(), ownersDevops)).Should(Succeed())
})
By("checking configuration", func() {
Eventually(func(g Gomega) {
cfg := &capsulev1beta2.CapsuleConfiguration{}
err := k8sClient.Get(
context.Background(),
client.ObjectKey{Name: defaultConfigurationName},
cfg,
)
g.Expect(err).ToNot(HaveOccurred())
expected := api.UserListSpec{
{Kind: tnt1Owner.Spec.Kind, Name: tnt1Owner.Spec.Name},
{Kind: api.GroupOwner, Name: "projectcapsule.dev"},
}
g.Expect(cfg.Status.Users).To(ConsistOf(expected))
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
By("checking owners (e2e-owners-1)", func() {
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed())
@@ -486,6 +749,20 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: userOwnersCommon.Spec.Kind,
Name: userOwnersCommon.Spec.Name,
},
ClusterRoles: []string{"service-admin"},
},
{
UserSpec: api.UserSpec{
Kind: tnt1Owner.Spec.Kind,
Name: tnt1Owner.Spec.Name,
},
ClusterRoles: []string{"service-admin"},
},
}
Expect(normalizeOwners(t.Status.Owners)).
@@ -520,6 +797,13 @@ var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() {
},
ClusterRoles: []string{"admin", "capsule-namespace-deleter"},
},
{
UserSpec: api.UserSpec{
Kind: userOwnersCommon.Spec.Kind,
Name: userOwnersCommon.Spec.Name,
},
ClusterRoles: []string{"service-admin"},
},
}
Expect(normalizeOwners(t.Status.Owners)).

View File

@@ -23,7 +23,7 @@ import (
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
)
var _ = Describe("enforcing a Runtime Class", Label("pod", "classes", "current"), func() {
var _ = Describe("enforcing a Runtime Class", Label("pod", "classes"), func() {
tntWithDefault := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "e2e-runtime-selection",

View File

@@ -20,7 +20,7 @@ import (
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/misc"
"github.com/projectcapsule/capsule/pkg/runtime/selectors"
"github.com/projectcapsule/capsule/pkg/utils"
)
@@ -85,7 +85,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []misc.NamespaceSelector{
Selectors: []selectors.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -243,7 +243,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err := k8sClient.Create(context.TODO(), claim1)
Expect(err).Should(Succeed(), "Failed to create Claim %s", claim1)
isSuccessfullyBoundToPool(pool, claim1)
isSuccessfullyBoundAndUnsedToPool(pool, claim1)
claim2 := &capsulev1beta2.ResourcePoolClaim{
ObjectMeta: metav1.ObjectMeta{
@@ -260,7 +260,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err = k8sClient.Create(context.TODO(), claim2)
Expect(err).Should(Succeed(), "Failed to create Claim %s", claim2)
isSuccessfullyBoundToPool(pool, claim2)
isSuccessfullyBoundAndUnsedToPool(pool, claim2)
claim3 := &capsulev1beta2.ResourcePoolClaim{
ObjectMeta: metav1.ObjectMeta{
@@ -524,7 +524,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []misc.NamespaceSelector{
Selectors: []selectors.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -696,7 +696,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []misc.NamespaceSelector{
Selectors: []selectors.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -769,7 +769,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err := k8sClient.Create(context.TODO(), claim)
Expect(err).Should(Succeed(), "Failed to create Claim %s", claim)
isSuccessfullyBoundToPool(pool, claim)
isSuccessfullyBoundAndUnsedToPool(pool, claim)
})
By("Verify Status was correctly initialized", func() {
@@ -834,16 +834,16 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
conditions := extractResourcePoolMessage(claim.Status.Condition.Message)
expected := []string{
"requested.requests.cpu=4",
"available.requests.cpu=2",
}
Expect(containsAll(conditions, expected)).To(BeTrue(), "Actual message"+claim.Status.Condition.Message)
Expect(claim.Status.Condition.Reason).To(Equal(meta.PoolExhaustedReason))
Expect(claim.Status.Condition.Status).To(Equal(metav1.ConditionFalse))
Expect(claim.Status.Condition.Type).To(Equal(meta.BoundCondition))
exhausted := claim.Status.Conditions.GetConditionByType(meta.ExhaustedCondition)
Expect(containsAll(extractResourcePoolMessage(exhausted.Message), expected)).To(BeTrue(), "Actual message"+exhausted.Message)
Expect(exhausted.Reason).To(Equal(meta.PoolExhaustedReason))
Expect(exhausted.Status).To(Equal(metav1.ConditionTrue))
Expect(exhausted.Type).To(Equal(meta.ExhaustedCondition))
})
By("Create claim for request.memory", func() {
@@ -862,7 +862,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err := k8sClient.Create(context.TODO(), claim)
Expect(err).Should(Succeed(), "Failed to create Claim %s", claim)
isSuccessfullyBoundToPool(pool, claim)
isSuccessfullyBoundAndUnsedToPool(pool, claim)
})
By("Verify Status was correctly initialized", func() {
@@ -905,7 +905,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err := k8sClient.Create(context.TODO(), claim)
Expect(err).Should(Succeed(), "Failed to create Claim %s", claim)
isSuccessfullyBoundToPool(pool, claim)
isSuccessfullyBoundAndUnsedToPool(pool, claim)
})
By("Verify Status was correctly initialized", func() {
@@ -942,16 +942,15 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
conditions := extractResourcePoolMessage(claim.Status.Condition.Message)
exhausted := claim.Status.Conditions.GetConditionByType(meta.ExhaustedCondition)
expected := []string{
"requested.requests.cpu=4",
"available.requests.cpu=0",
}
Expect(containsAll(conditions, expected)).To(BeTrue(), "Actual message"+claim.Status.Condition.Message)
Expect(claim.Status.Condition.Reason).To(Equal(meta.PoolExhaustedReason))
Expect(claim.Status.Condition.Status).To(Equal(metav1.ConditionFalse))
Expect(claim.Status.Condition.Type).To(Equal(meta.BoundCondition))
Expect(containsAll(extractResourcePoolMessage(exhausted.Message), expected)).To(BeTrue(), "Actual message"+claim.Status.Condition.Message)
Expect(exhausted.Reason).To(Equal(meta.PoolExhaustedReason))
Expect(exhausted.Status).To(Equal(metav1.ConditionTrue))
})
})
@@ -964,7 +963,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []misc.NamespaceSelector{
Selectors: []selectors.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -1037,7 +1036,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err := k8sClient.Create(context.TODO(), claim)
Expect(err).Should(Succeed(), "Failed to create Claim %s", claim)
isSuccessfullyBoundToPool(pool, claim)
isSuccessfullyBoundAndUnsedToPool(pool, claim)
})
By("Create claim for requests.requests", func() {
@@ -1056,7 +1055,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err := k8sClient.Create(context.TODO(), claim)
Expect(err).Should(Succeed(), "Failed to create Claim %s", claim)
isSuccessfullyBoundToPool(pool, claim)
isSuccessfullyBoundAndUnsedToPool(pool, claim)
})
By("Verify Status was correctly initialized", func() {
@@ -1104,16 +1103,15 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
conditions := extractResourcePoolMessage(claim.Status.Condition.Message)
exhausted := claim.Status.Conditions.GetConditionByType(meta.ExhaustedCondition)
expected := []string{
"requested.requests.cpu=4",
"available.requests.cpu=2",
}
Expect(containsAll(conditions, expected)).To(BeTrue(), "Actual message"+claim.Status.Condition.Message)
Expect(claim.Status.Condition.Reason).To(Equal(meta.PoolExhaustedReason))
Expect(claim.Status.Condition.Status).To(Equal(metav1.ConditionFalse))
Expect(claim.Status.Condition.Type).To(Equal(meta.BoundCondition))
Expect(containsAll(extractResourcePoolMessage(exhausted.Message), expected)).To(BeTrue(), "Actual message"+exhausted.Message)
Expect(exhausted.Reason).To(Equal(meta.PoolExhaustedReason))
Expect(exhausted.Status).To(Equal(metav1.ConditionTrue))
})
By("Create claim exhausting limits.cpu", func() {
@@ -1137,16 +1135,15 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
conditions := extractResourcePoolMessage(claim.Status.Condition.Message)
exhausted := claim.Status.Conditions.GetConditionByType(meta.ExhaustedCondition)
expected := []string{
"requested.limits.cpu=4",
"available.limits.cpu=2",
}
Expect(containsAll(conditions, expected)).To(BeTrue(), "Actual message"+claim.Status.Condition.Message)
Expect(claim.Status.Condition.Reason).To(Equal(meta.PoolExhaustedReason))
Expect(claim.Status.Condition.Status).To(Equal(metav1.ConditionFalse))
Expect(claim.Status.Condition.Type).To(Equal(meta.BoundCondition))
Expect(containsAll(extractResourcePoolMessage(exhausted.Message), expected)).To(BeTrue(), "Actual message"+exhausted.Message)
Expect(exhausted.Reason).To(Equal(meta.PoolExhaustedReason))
Expect(exhausted.Status).To(Equal(metav1.ConditionTrue))
})
By("Create claim for requests.cpu (attempt to skip exhausting one)", func() {
@@ -1171,7 +1168,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
conditions := extractResourcePoolMessage(claim.Status.Condition.Message)
exhausted := claim.Status.Conditions.GetConditionByType(meta.ExhaustedCondition)
expected := []string{
"requested.limits.cpu=2",
"queued.limits.cpu=4",
@@ -1179,10 +1176,9 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
"queued.requests.cpu=4",
}
Expect(containsAll(conditions, expected)).To(BeTrue(), "Actual message"+claim.Status.Condition.Message)
Expect(claim.Status.Condition.Reason).To(Equal(meta.QueueExhaustedReason))
Expect(claim.Status.Condition.Status).To(Equal(metav1.ConditionFalse))
Expect(claim.Status.Condition.Type).To(Equal(meta.BoundCondition))
Expect(containsAll(extractResourcePoolMessage(exhausted.Message), expected)).To(BeTrue(), "Actual message"+exhausted.Message)
Expect(exhausted.Reason).To(Equal(meta.QueueExhaustedReason))
Expect(exhausted.Status).To(Equal(metav1.ConditionTrue))
})
By("Verify ResourceQuotas for namespaces", func() {
@@ -1257,7 +1253,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: "simple-2", Namespace: "ns-2-pool-ordered"}, claim)
Expect(err).Should(Succeed(), "Failed to create Claim %s", claim)
isSuccessfullyBoundToPool(pool, claim)
isSuccessfullyBoundAndUnsedToPool(pool, claim)
})
By("Verify queued claim can be allocated", func() {
@@ -1266,7 +1262,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: "simple-3", Namespace: "ns-1-pool-ordered"}, claim)
Expect(err).Should(Succeed(), "Failed to create Claim %s", claim)
isSuccessfullyBoundToPool(pool, claim)
isSuccessfullyBoundAndUnsedToPool(pool, claim)
})
By("Verify ResourceQuotas for namespaces", func() {
@@ -1309,7 +1305,8 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
conditions := extractResourcePoolMessage(claim.Status.Condition.Message)
exhausted := claim.Status.Conditions.GetConditionByType(meta.ExhaustedCondition)
expected := []string{
"requested.limits.cpu=2",
"available.limits.cpu=0",
@@ -1317,10 +1314,9 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
"available.requests.cpu=0",
}
Expect(containsAll(conditions, expected)).To(BeTrue(), "Actual message"+claim.Status.Condition.Message)
Expect(claim.Status.Condition.Reason).To(Equal(meta.PoolExhaustedReason))
Expect(claim.Status.Condition.Status).To(Equal(metav1.ConditionFalse))
Expect(claim.Status.Condition.Type).To(Equal(meta.BoundCondition))
Expect(containsAll(extractResourcePoolMessage(exhausted.Message), expected)).To(BeTrue(), "Actual message"+exhausted.Message)
Expect(exhausted.Reason).To(Equal(meta.PoolExhaustedReason))
Expect(exhausted.Status).To(Equal(metav1.ConditionTrue))
})
})
@@ -1333,7 +1329,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []misc.NamespaceSelector{
Selectors: []selectors.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -1414,14 +1410,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err := k8sClient.Create(context.TODO(), claim)
Expect(err).Should(Succeed(), "Failed to create Claim %s", claim)
isSuccessfullyBoundToPool(pool, claim)
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
Expect(claim.Status.Condition.Reason).To(Equal(meta.SucceededReason))
Expect(claim.Status.Condition.Status).To(Equal(metav1.ConditionTrue))
Expect(claim.Status.Condition.Type).To(Equal(meta.BoundCondition))
isSuccessfullyBoundAndUnsedToPool(pool, claim)
})
By("Create claim non matching namespace", func() {
@@ -1446,9 +1435,11 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
Expect(claim.Status.Condition.Reason).To(Equal(meta.FailedReason))
Expect(claim.Status.Condition.Status).To(Equal(metav1.ConditionFalse))
Expect(claim.Status.Condition.Type).To(Equal(meta.AssignedCondition))
assigned := claim.Status.Conditions.GetConditionByType(meta.ReadyCondition)
Expect(assigned.Reason).To(Equal(meta.FailedReason))
Expect(assigned.Status).To(Equal(metav1.ConditionFalse))
Expect(assigned.Type).To(Equal(meta.ReadyCondition))
})
By("Update Namespace Labels to become matching", func() {
@@ -1477,7 +1468,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
isSuccessfullyBoundToPool(pool, claim)
isSuccessfullyBoundAndUnsedToPool(pool, claim)
})
})
@@ -1491,7 +1482,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []misc.NamespaceSelector{
Selectors: []selectors.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -1599,7 +1590,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []misc.NamespaceSelector{
Selectors: []selectors.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -1707,7 +1698,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []misc.NamespaceSelector{
Selectors: []selectors.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -1779,14 +1770,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err := k8sClient.Create(context.TODO(), claim)
Expect(err).Should(Succeed(), "Failed to create Claim %s", claim)
isSuccessfullyBoundToPool(pool, claim)
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
Expect(claim.Status.Condition.Reason).To(Equal(meta.SucceededReason))
Expect(claim.Status.Condition.Status).To(Equal(metav1.ConditionTrue))
Expect(claim.Status.Condition.Type).To(Equal(meta.BoundCondition))
isSuccessfullyBoundAndUnsedToPool(pool, claim)
})
By("Create claims", func() {
@@ -1805,14 +1789,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
err := k8sClient.Create(context.TODO(), claim)
Expect(err).Should(Succeed(), "Failed to create Claim %s", claim)
isSuccessfullyBoundToPool(pool, claim)
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
Expect(claim.Status.Condition.Reason).To(Equal(meta.SucceededReason))
Expect(claim.Status.Condition.Status).To(Equal(metav1.ConditionTrue))
Expect(claim.Status.Condition.Type).To(Equal(meta.BoundCondition))
isSuccessfullyBoundAndUnsedToPool(pool, claim)
})
By("Verify ResourcePool Status Allocation", func() {
@@ -2026,7 +2003,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() {
})
})
func isSuccessfullyBoundToPool(pool *capsulev1beta2.ResourcePool, claim *capsulev1beta2.ResourcePoolClaim) {
func isSuccessfullyBoundAndUnsedToPool(pool *capsulev1beta2.ResourcePool, claim *capsulev1beta2.ResourcePoolClaim) {
fetchedPool := &capsulev1beta2.ResourcePool{}
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: pool.Name}, fetchedPool)
Expect(err).Should(Succeed())
@@ -2040,9 +2017,32 @@ func isSuccessfullyBoundToPool(pool *capsulev1beta2.ResourcePool, claim *capsule
Expect(fetchedClaim.Status.Pool.Name.String()).To(Equal(fetchedPool.Name))
Expect(fetchedClaim.Status.Pool.UID).To(Equal(fetchedPool.GetUID()))
Expect(fetchedClaim.Status.Condition.Type).To(Equal(meta.BoundCondition))
Expect(fetchedClaim.Status.Condition.Status).To(Equal(metav1.ConditionTrue))
Expect(fetchedClaim.Status.Condition.Reason).To(Equal(meta.SucceededReason))
bound := fetchedClaim.Status.Conditions.GetConditionByType(meta.BoundCondition)
Expect(bound.Type).To(Equal(meta.BoundCondition))
Expect(bound.Status).To(Equal(metav1.ConditionFalse))
Expect(bound.Reason).To(Equal(meta.UnusedReason))
}
func isSuccessfullyBoundAndUsedToPool(pool *capsulev1beta2.ResourcePool, claim *capsulev1beta2.ResourcePoolClaim) {
fetchedPool := &capsulev1beta2.ResourcePool{}
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: pool.Name}, fetchedPool)
Expect(err).Should(Succeed())
fetchedClaim := &capsulev1beta2.ResourcePoolClaim{}
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, fetchedClaim)
Expect(err).Should(Succeed())
isBoundToPool(fetchedPool, fetchedClaim)
Expect(fetchedClaim.Status.Pool.Name.String()).To(Equal(fetchedPool.Name))
Expect(fetchedClaim.Status.Pool.UID).To(Equal(fetchedPool.GetUID()))
bound := fetchedClaim.Status.Conditions.GetConditionByType(meta.BoundCondition)
Expect(bound.Type).To(Equal(meta.BoundCondition))
Expect(bound.Status).To(Equal(metav1.ConditionTrue))
Expect(bound.Reason).To(Equal(meta.InUseReason))
}
func isBoundToPool(pool *capsulev1beta2.ResourcePool, claim *capsulev1beta2.ResourcePoolClaim) bool {

View File

@@ -5,10 +5,12 @@ package e2e
import (
"context"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
@@ -17,7 +19,7 @@ import (
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/misc"
"github.com/projectcapsule/capsule/pkg/runtime/selectors"
)
var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
@@ -102,7 +104,7 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
},
},
Spec: capsulev1beta2.ResourcePoolSpec{
Selectors: []misc.NamespaceSelector{
Selectors: []selectors.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -230,16 +232,7 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim1.Name, Namespace: claim1.Namespace}, claim1)
Expect(err).Should(Succeed())
isSuccessfullyBoundToPool(pool1, claim1)
expectedPool := api.StatusNameUID{
Name: api.Name(pool1.Name),
UID: pool1.GetUID(),
}
Expect(claim1.Status.Pool).To(Equal(expectedPool), "expected pool name to match")
Expect(claim1.Status.Condition.Status).To(Equal(metav1.ConditionTrue), "failed to verify condition status")
Expect(claim1.Status.Condition.Type).To(Equal(meta.BoundCondition), "failed to verify condition type")
Expect(claim1.Status.Condition.Reason).To(Equal(meta.SucceededReason), "failed to verify condition reason")
isSuccessfullyBoundAndUnsedToPool(pool1, claim1)
})
By("Create a second claim and verify binding", func() {
@@ -249,16 +242,7 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim2.Name, Namespace: claim2.Namespace}, claim2)
Expect(err).Should(Succeed())
isSuccessfullyBoundToPool(pool1, claim2)
expectedPool := api.StatusNameUID{
Name: api.Name(pool1.Name),
UID: pool1.GetUID(),
}
Expect(claim2.Status.Pool).To(Equal(expectedPool), "expected pool name to match")
Expect(claim2.Status.Condition.Status).To(Equal(metav1.ConditionTrue), "failed to verify condition status")
Expect(claim2.Status.Condition.Type).To(Equal(meta.BoundCondition), "failed to verify condition type")
Expect(claim2.Status.Condition.Reason).To(Equal(meta.SucceededReason), "failed to verify condition reason")
isSuccessfullyBoundAndUnsedToPool(pool1, claim2)
})
By("Create a third claim and verify error", func() {
@@ -284,12 +268,15 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
expectedPool := api.StatusNameUID{}
expectedPool := meta.LocalRFC1123ObjectReferenceWithUID{}
Expect(claim.Status.Pool).To(Equal(expectedPool), "expected pool name to be empty")
Expect(claim.Status.Condition.Status).To(Equal(metav1.ConditionFalse), "failed to verify condition status")
Expect(claim.Status.Condition.Type).To(Equal(meta.AssignedCondition), "failed to verify condition type")
Expect(claim.Status.Condition.Reason).To(Equal(meta.FailedReason), "failed to verify condition reason")
Expect(len(claim.Status.Conditions)).To(Equal(1), "expected single condition")
Expect(len(claim.OwnerReferences)).To(Equal(0), "expected no ownerreferences")
assigned := claim.Status.Conditions.GetConditionByType(meta.ReadyCondition)
Expect(assigned.Status).To(Equal(metav1.ConditionFalse), "failed to verify condition status")
Expect(assigned.Type).To(Equal(meta.ReadyCondition), "failed to verify condition type")
Expect(assigned.Reason).To(Equal(meta.FailedReason), "failed to verify condition reason")
})
})
@@ -305,7 +292,7 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
Config: capsulev1beta2.ResourcePoolSpecConfiguration{
DeleteBoundResources: ptr.To(false),
},
Selectors: []misc.NamespaceSelector{
Selectors: []selectors.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -373,20 +360,56 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
expectedPool := api.StatusNameUID{
Name: api.Name(pool.Name),
expectedPool := meta.LocalRFC1123ObjectReferenceWithUID{
Name: meta.RFC1123Name(pool.Name),
UID: pool.GetUID(),
}
isBoundCondition(claim)
isBoundAndUnusedCondition(claim)
Expect(claim.Status.Pool).To(Equal(expectedPool), "expected pool name to match")
})
By("Error on deleting bound claim", func() {
By("Create a pod with resource requests/limits", func() {
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "claim-pod",
Namespace: claim.Namespace,
Labels: map[string]string{
"e2e": "claim-pod",
},
},
Spec: corev1.PodSpec{
// optional: helps schedule quickly, avoid restarts
RestartPolicy: corev1.RestartPolicyNever,
Containers: []corev1.Container{
{
Name: "pause",
Image: "registry.k8s.io/pause:3.9",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("10m"),
corev1.ResourceMemory: resource.MustParse("16Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("20m"),
corev1.ResourceMemory: resource.MustParse("32Mi"),
},
},
},
},
},
}
Expect(k8sClient.Create(context.TODO(), pod)).To(Succeed())
})
By("Verify the claim is used", func() {
time.Sleep(250 * time.Millisecond)
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
isBoundCondition(claim)
isBoundAndUsedCondition(claim)
err = k8sClient.Delete(context.TODO(), claim)
Expect(err).ShouldNot(Succeed())
@@ -432,6 +455,82 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
Expect(err).ShouldNot(Succeed(), "Expected error when updating resources in bound state %s", claim)
})
By("Make the claim unused", func() {
key := client.ObjectKey{Name: "claim-pod", Namespace: claim.Namespace}
pod := &corev1.Pod{}
err := k8sClient.Get(context.TODO(), key, pod)
Expect(err).To(Succeed(), "pod must exist before deleting")
Expect(k8sClient.Delete(context.TODO(), pod, &client.DeleteOptions{
GracePeriodSeconds: ptr.To(int64(0)),
})).To(Succeed())
Eventually(func() bool {
p := &corev1.Pod{}
err := k8sClient.Get(
context.TODO(),
key,
p,
)
return apierrors.IsNotFound(err)
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
})
By("Bind a claim", func() {
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
expectedPool := meta.LocalRFC1123ObjectReferenceWithUID{
Name: meta.RFC1123Name(pool.Name),
UID: pool.GetUID(),
}
isBoundAndUnusedCondition(claim)
Expect(claim.Status.Pool).To(Equal(expectedPool), "expected pool name to match")
})
By("Allow 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())
claim.Spec.ResourceClaims = corev1.ResourceList{
corev1.ResourceLimitsCPU: resource.MustParse("2"),
corev1.ResourceLimitsMemory: resource.MustParse("2Gi"),
corev1.ResourceRequestsCPU: resource.MustParse("2"),
corev1.ResourceRequestsMemory: resource.MustParse("2Gi"),
}
err = k8sClient.Update(context.TODO(), claim)
Expect(err).Should(Succeed(), "Expected error when updating resources in bound state %s", claim)
})
By("Allow on patching resources for claim (Decrease)", func() {
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
claim.Spec.ResourceClaims = corev1.ResourceList{
corev1.ResourceLimitsCPU: resource.MustParse("0"),
corev1.ResourceLimitsMemory: resource.MustParse("0Gi"),
corev1.ResourceRequestsCPU: resource.MustParse("0"),
corev1.ResourceRequestsMemory: resource.MustParse("0Gi"),
}
err = k8sClient.Update(context.TODO(), claim)
Expect(err).Should(Succeed(), "Expected error when updating resources in bound state %s", claim)
})
By("Allow on patching pool name", func() {
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
Expect(err).Should(Succeed())
claim.Spec.Pool = "some-random-pool"
err = k8sClient.Update(context.TODO(), claim)
Expect(err).Should(Succeed(), "Expected error when updating resources in bound state %s", claim)
})
By("Delete Pool", func() {
err := k8sClient.Delete(context.TODO(), pool)
Expect(err).Should(Succeed())
@@ -495,7 +594,7 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
Config: capsulev1beta2.ResourcePoolSpecConfiguration{
DeleteBoundResources: ptr.To(false),
},
Selectors: []misc.NamespaceSelector{
Selectors: []selectors.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -524,7 +623,7 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
Config: capsulev1beta2.ResourcePoolSpecConfiguration{
DeleteBoundResources: ptr.To(false),
},
Selectors: []misc.NamespaceSelector{
Selectors: []selectors.NamespaceSelector{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
@@ -666,17 +765,33 @@ func isUnassignedCondition(claim *capsulev1beta2.ResourcePoolClaim) {
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, cl)
Expect(err).Should(Succeed())
Expect(cl.Status.Condition.Status).To(Equal(metav1.ConditionFalse), "failed to verify condition status")
Expect(cl.Status.Condition.Type).To(Equal(meta.AssignedCondition), "failed to verify condition type")
Expect(cl.Status.Condition.Reason).To(Equal(meta.FailedReason), "failed to verify condition reason")
assigned := cl.Status.Conditions.GetConditionByType(meta.ReadyCondition)
Expect(assigned.Status).To(Equal(metav1.ConditionFalse), "failed to verify condition status")
Expect(assigned.Type).To(Equal(meta.ReadyCondition), "failed to verify condition type")
Expect(assigned.Reason).To(Equal(meta.FailedReason), "failed to verify condition reason")
}
func isBoundCondition(claim *capsulev1beta2.ResourcePoolClaim) {
func isBoundAndUnusedCondition(claim *capsulev1beta2.ResourcePoolClaim) {
cl := &capsulev1beta2.ResourcePoolClaim{}
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, cl)
Expect(err).Should(Succeed())
Expect(cl.Status.Condition.Status).To(Equal(metav1.ConditionTrue), "failed to verify condition status")
Expect(cl.Status.Condition.Type).To(Equal(meta.BoundCondition), "failed to verify condition type")
Expect(cl.Status.Condition.Reason).To(Equal(meta.SucceededReason), "failed to verify condition reason")
bound := cl.Status.Conditions.GetConditionByType(meta.BoundCondition)
Expect(bound.Type).To(Equal(meta.BoundCondition), "failed to verify condition type")
Expect(bound.Reason).To(Equal(meta.UnusedReason), "failed to verify condition reason")
Expect(bound.Status).To(Equal(metav1.ConditionFalse), "failed to verify condition status")
}
func isBoundAndUsedCondition(claim *capsulev1beta2.ResourcePoolClaim) {
cl := &capsulev1beta2.ResourcePoolClaim{}
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, cl)
Expect(err).Should(Succeed())
bound := cl.Status.Conditions.GetConditionByType(meta.BoundCondition)
Expect(bound.Status).To(Equal(metav1.ConditionTrue), "failed to verify condition status")
Expect(bound.Type).To(Equal(meta.BoundCondition), "failed to verify condition type")
Expect(bound.Reason).To(Equal(meta.InUseReason), "failed to verify condition reason")
}

158
e2e/rules_managed_test.go Normal file
View File

@@ -0,0 +1,158 @@
package e2e
import (
"context"
"fmt"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api"
"github.com/projectcapsule/capsule/pkg/api/meta"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
var _ = Describe("NamespaceStatus objects", Label("tenant", "rules"), func() {
ctx := context.Background()
// Two tenants, each with one owner (reuse your existing ownerClient/NamespaceCreation helpers)
tntA := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{Name: "nsstatus-a"},
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{Name: "matt", Kind: "User"},
},
},
},
},
}
tntB := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{Name: "nsstatus-b"},
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{Name: "matt", Kind: "User"},
},
},
},
},
}
var (
nsA1 *corev1.Namespace
nsA2 *corev1.Namespace
nsB1 *corev1.Namespace
)
JustBeforeEach(func() {
// Create tenants
EventuallyCreation(func() error {
tntA.ResourceVersion = ""
return k8sClient.Create(ctx, tntA)
}).Should(Succeed())
EventuallyCreation(func() error {
tntB.ResourceVersion = ""
return k8sClient.Create(ctx, tntB)
}).Should(Succeed())
// Create namespaces for each tenant using your helper
nsA1 = NewNamespace("rule-status-ns1", map[string]string{
meta.TenantLabel: tntA.GetName(),
})
nsA2 = NewNamespace("rule-status-ns2", map[string]string{
meta.TenantLabel: tntA.GetName(),
})
nsB1 = NewNamespace("rule-status-ns3", map[string]string{
meta.TenantLabel: tntB.GetName(),
})
NamespaceCreation(nsA1, tntA.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(nsA2, tntA.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
NamespaceCreation(nsB1, tntB.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
// Wait until tenants list their namespaces (optional but makes debugging easier)
TenantNamespaceList(tntA, defaultTimeoutInterval).Should(ContainElements(nsA1.GetName(), nsA2.GetName()))
TenantNamespaceList(tntB, defaultTimeoutInterval).Should(ContainElement(nsB1.GetName()))
})
JustAfterEach(func() {
// Best-effort cleanup namespaces first (your env may already handle this)
for _, n := range []*corev1.Namespace{nsA1, nsA2, nsB1} {
if n == nil {
continue
}
_ = k8sClient.Delete(ctx, n)
}
// Delete tenants
if tntA != nil {
_ = k8sClient.Delete(ctx, tntA)
}
if tntB != nil {
_ = k8sClient.Delete(ctx, tntB)
}
})
// --- Helpers ---
expectNamespaceStatusFor := func(ns *corev1.Namespace, tenantName string) {
By(fmt.Sprintf("verifying NamespaceStatus for namespace %q (tenant=%q)", ns.Name, tenantName))
Eventually(func(g Gomega) {
// Re-read namespace to get UID reliably (in case local object is stale)
curNS := &corev1.Namespace{}
g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: ns.Name}, curNS)).To(Succeed())
nsStatus := &capsulev1beta2.RuleStatus{}
g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: meta.NameForManagedRuleStatus(), Namespace: ns.Name}, nsStatus)).To(Succeed())
// 2) OwnerReference must point to the Namespace and be controller owner
g.Expect(nsStatus.OwnerReferences).NotTo(BeEmpty())
var found bool
for _, or := range nsStatus.OwnerReferences {
if or.APIVersion == "v1" &&
or.Kind == "Namespace" &&
or.Name == curNS.Name &&
or.UID == curNS.UID {
found = true
break
}
}
g.Expect(found).To(BeTrue(), "expected NamespaceStatus to have Namespace controller OwnerReference")
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
It("creates one NamespaceStatus per namespace, with correct Status.Tenant and Namespace controller OwnerReference", func() {
expectNamespaceStatusFor(nsA1, tntA.Name)
expectNamespaceStatusFor(nsA2, tntA.Name)
expectNamespaceStatusFor(nsB1, tntB.Name)
})
It("removes NamespaceStatus when the Namespace is deleted (ownerReference GC)", func() {
// Ensure it exists first
expectNamespaceStatusFor(nsA1, tntA.Name)
// Delete namespace
Expect(k8sClient.Delete(ctx, nsA1)).To(Succeed())
// Namespace deletion can take time; once it's gone, the status should be GC'd
Eventually(func() bool {
// confirm namespace gone or terminating; either way, check status disappears eventually
nsStatus := &capsulev1beta2.RuleStatus{}
err := k8sClient.Get(ctx, client.ObjectKey{Name: meta.NameForManagedRuleStatus(), Namespace: nsA1.Name}, nsStatus)
return apierrors.IsNotFound(err)
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
})
})

525
e2e/rules_registry_test.go Normal file
View File

@@ -0,0 +1,525 @@
// Copyright 2020-2023 Project Capsule Authors.
// SPDX-License-Identifier: Apache-2.0
package e2e
import (
"context"
"fmt"
"strings"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"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/api/meta"
)
var _ = Describe("enforcing a Container Registry", Label("tenant", "rules", "images", "registry"), func() {
originConfig := &capsulev1beta2.CapsuleConfiguration{}
tnt := &capsulev1beta2.Tenant{
ObjectMeta: metav1.ObjectMeta{
Name: "container-registry",
},
Spec: capsulev1beta2.TenantSpec{
Owners: api.OwnerListSpec{
{
CoreOwnerSpec: api.CoreOwnerSpec{
UserSpec: api.UserSpec{
Name: "matt",
Kind: "User",
},
},
},
},
Rules: []*capsulev1beta2.NamespaceRule{
{
NamespaceRuleBody: capsulev1beta2.NamespaceRuleBody{
Enforce: capsulev1beta2.NamespaceRuleEnforceBody{
Registries: []api.OCIRegistry{
// Global: allow any registry, but require PullPolicy Always (images+volumes)
{
Registry: ".*",
Validation: []api.RegistryValidationTarget{
api.ValidateImages,
api.ValidateVolumes,
},
Policy: []corev1.PullPolicy{corev1.PullAlways},
},
// More specific harbor rule (no policy override => should NOT remove Always restriction)
{
Registry: "harbor/.*",
Validation: []api.RegistryValidationTarget{
api.ValidateImages,
api.ValidateVolumes,
},
},
},
},
},
},
{
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"environment": "prod",
},
},
NamespaceRuleBody: capsulev1beta2.NamespaceRuleBody{
Enforce: capsulev1beta2.NamespaceRuleEnforceBody{
Registries: []api.OCIRegistry{
// Prod-only special-case
{
Registry: "harbor/production-image/.*",
Validation: []api.RegistryValidationTarget{
api.ValidateImages,
api.ValidateVolumes,
},
Policy: []corev1.PullPolicy{corev1.PullAlways},
},
},
},
},
},
},
},
}
// ---- Small local helpers (keep e2e readable) ----
expectNamespaceStatusRegistries := func(nsName string, want []string) {
Eventually(func(g Gomega) {
nsStatus := &capsulev1beta2.RuleStatus{}
g.Expect(k8sClient.Get(
context.Background(),
client.ObjectKey{Name: meta.NameForManagedRuleStatus(), Namespace: nsName},
nsStatus,
)).To(Succeed())
got := make([]string, 0, len(nsStatus.Status.Rule.Enforce.Registries))
for _, r := range nsStatus.Status.Rule.Enforce.Registries {
got = append(got, r.Registry)
}
g.Expect(got).To(Equal(want))
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
createPodAndExpectDenied := func(cs kubernetes.Interface, nsName string, pod *corev1.Pod, substrings ...string) {
base := pod.DeepCopy()
baseName := base.Name
if baseName == "" {
baseName = "pod"
}
Eventually(func() error {
// unique name per attempt to avoid AlreadyExists
p := base.DeepCopy()
p.Name = fmt.Sprintf("%s-%d", baseName, int(time.Now().UnixNano()%1e6))
_, err := cs.CoreV1().Pods(nsName).Create(context.Background(), p, metav1.CreateOptions{})
if err == nil {
_ = cs.CoreV1().Pods(nsName).Delete(context.Background(), p.Name, metav1.DeleteOptions{})
return fmt.Errorf("expected create to be denied, but it succeeded")
}
if apierrors.IsAlreadyExists(err) {
return fmt.Errorf("unexpected AlreadyExists: %v", err)
}
msg := err.Error()
for _, s := range substrings {
if !strings.Contains(msg, s) {
return fmt.Errorf("expected error to contain %q, got: %s", s, msg)
}
}
return nil
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
}
createPodAndExpectAllowed := func(cs kubernetes.Interface, nsName string, pod *corev1.Pod) {
EventuallyCreation(func() error {
_, err := cs.CoreV1().Pods(nsName).Create(context.Background(), pod, metav1.CreateOptions{})
return err
}).Should(Succeed())
}
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
}
c.Spec = originConfig.Spec
return k8sClient.Update(context.Background(), c)
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
It("aggregates enforcement rules into NamespaceStatus for a non-prod namespace", func() {
ns := NewNamespace("")
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
// Non-prod: should include only the global rule body (two registries in order)
expectNamespaceStatusRegistries(ns.GetName(), []string{
".*",
"harbor/.*",
})
// Sanity: we can still create a trivial pod with explicit Always (since global allows all registries)
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "sanity"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "c", Image: "gcr.io/google_containers/pause-amd64:3.0", ImagePullPolicy: corev1.PullAlways},
},
},
}
createPodAndExpectAllowed(cs, ns.Name, pod)
})
It("aggregates enforcement rules into NamespaceStatus for a prod namespace", func() {
ns := NewNamespace("", map[string]string{
"environment": "prod",
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
// Prod: should include global + prod rule (3 registries in order)
expectNamespaceStatusRegistries(ns.GetName(), []string{
".*",
"harbor/.*",
"harbor/production-image/.*",
})
// Sanity allow with Always
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "prod-sanity"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "c", Image: "harbor/production-image/app:1", ImagePullPolicy: corev1.PullAlways},
},
},
}
createPodAndExpectAllowed(cs, ns.Name, pod)
})
It("denies a container image when pullPolicy is not explicitly set under restriction (dev)", func() {
ns := NewNamespace("")
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
// No ImagePullPolicy set => "" => should be denied because global rule restricts policy to Always
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "no-pullpolicy"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "c", Image: "gcr.io/google_containers/pause-amd64:3.0"},
},
},
}
createPodAndExpectDenied(cs, ns.Name, pod,
"uses pullPolicy=IfNotPresent",
"not allowed",
"allowed: Always",
)
})
It("denies a harbor image with pullPolicy IfNotPresent because global Always must still apply (dev)", func() {
ns := NewNamespace("")
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "harbor-wrong-policy"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "c",
Image: "harbor/some-team/app:1",
ImagePullPolicy: corev1.PullIfNotPresent,
},
},
},
}
createPodAndExpectDenied(cs, ns.Name, pod,
"pullPolicy=IfNotPresent",
"not allowed",
"allowed:",
)
})
It("allows a harbor image with pullPolicy Always (dev)", func() {
ns := NewNamespace("")
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "harbor-always"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "c",
Image: "harbor/some-team/app:1",
ImagePullPolicy: corev1.PullAlways,
},
},
},
}
createPodAndExpectAllowed(cs, ns.Name, pod)
})
It("denies initContainers when they violate policy (dev) and includes the correct location in the message", func() {
ns := NewNamespace("")
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "init-deny"},
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{
{
Name: "init",
Image: "harbor/some-team/init:1",
ImagePullPolicy: corev1.PullIfNotPresent, // should be denied
},
},
Containers: []corev1.Container{
{
Name: "c",
Image: "harbor/some-team/app:1",
ImagePullPolicy: corev1.PullAlways,
},
},
},
}
createPodAndExpectDenied(cs, ns.Name, pod,
"initContainers[0]",
"pullPolicy=IfNotPresent",
"allowed:",
)
})
It("denies volume image pullPolicy if not allowed (dev)", func() {
ns := NewNamespace("")
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "volume-deny"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
// main container must exist
{Name: "c", Image: "harbor/some-team/app:1", ImagePullPolicy: corev1.PullAlways},
},
Volumes: []corev1.Volume{
{
Name: "imgvol",
VolumeSource: corev1.VolumeSource{
Image: &corev1.ImageVolumeSource{
Reference: "harbor/some-team/volimg:1",
PullPolicy: corev1.PullIfNotPresent, // should be denied
},
},
},
},
},
}
createPodAndExpectDenied(cs, ns.Name, pod,
"volumes[0](imgvol)",
"pullPolicy=IfNotPresent",
"allowed:",
)
})
It("allows prod-specific image only with Always, still enforcing global policy", func() {
ns := NewNamespace("", map[string]string{
"environment": "prod",
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
// Wrong policy => denied
bad := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "prod-bad"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "c", Image: "harbor/production-image/app:1", ImagePullPolicy: corev1.PullNever},
},
},
}
createPodAndExpectDenied(cs, ns.Name, bad,
"pullPolicy=Never",
"allowed:",
)
// Correct policy => allowed
good := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "prod-good"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "c", Image: "harbor/production-image/app:1", ImagePullPolicy: corev1.PullAlways},
},
},
}
createPodAndExpectAllowed(cs, ns.Name, good)
})
It("denies adding an ephemeral container with wrong pullPolicy on UPDATE", func() {
ns := NewNamespace("")
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
expectNamespaceStatusRegistries(ns.GetName(), []string{".*", "harbor/.*"})
cleanupRBAC := GrantEphemeralContainersUpdate(ns.Name, tnt.Spec.Owners[0].UserSpec.Name)
defer cleanupRBAC()
// Create an allowed pod
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "base"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "c", Image: "harbor/some-team/app:1", ImagePullPolicy: corev1.PullAlways},
},
},
}
createPodAndExpectAllowed(cs, ns.Name, pod)
// Now attempt to add an ephemeral container with IfNotPresent (should be denied)
ephem := corev1.EphemeralContainer{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "debug",
Image: "harbor/some-team/debug:1",
ImagePullPolicy: corev1.PullIfNotPresent,
},
}
Eventually(func() error {
// Must use the ephemeralcontainers subresource
cur, err := cs.CoreV1().Pods(ns.Name).Get(context.Background(), pod.Name, metav1.GetOptions{})
if err != nil {
return err
}
cur.Spec.EphemeralContainers = append(cur.Spec.EphemeralContainers, ephem)
_, err = cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(
context.Background(),
cur.Name,
cur,
metav1.UpdateOptions{},
)
if err == nil {
return fmt.Errorf("expected UpdateEphemeralContainers to be denied, but it succeeded")
}
msg := err.Error()
// Your webhook reports "ephemeralContainers[0]" location
if !strings.Contains(msg, "ephemeralContainers") || !strings.Contains(msg, "pullPolicy=IfNotPresent") {
return fmt.Errorf("unexpected error: %v", err)
}
return nil
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
It("denies a pod when volume image reference changes to a disallowed pullPolicy (recreate)", func() {
ns := NewNamespace("")
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
NamespaceCreation(ns, tnt.Spec.Owners[0].UserSpec, defaultTimeoutInterval).Should(Succeed())
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
expectNamespaceStatusRegistries(ns.GetName(), []string{".*", "harbor/.*"})
pod1 := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "vol-ok"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "c", Image: "harbor/some-team/app:1", ImagePullPolicy: corev1.PullAlways},
},
Volumes: []corev1.Volume{
{
Name: "imgvol",
VolumeSource: corev1.VolumeSource{
Image: &corev1.ImageVolumeSource{
Reference: "harbor/some-team/volimg:1",
PullPolicy: corev1.PullAlways,
},
},
},
},
},
}
createPodAndExpectAllowed(cs, ns.Name, pod1)
pod2 := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "vol-bad"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "c", Image: "harbor/some-team/app:1", ImagePullPolicy: corev1.PullAlways},
},
Volumes: []corev1.Volume{
{
Name: "imgvol",
VolumeSource: corev1.VolumeSource{
Image: &corev1.ImageVolumeSource{
Reference: "harbor/some-team/volimg:2",
PullPolicy: corev1.PullIfNotPresent,
},
},
},
},
},
}
createPodAndExpectDenied(cs, ns.Name, pod2,
"volumes[0](imgvol)",
"pullPolicy=IfNotPresent",
"allowed:",
)
})
})

View File

@@ -119,7 +119,7 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label("
if saCopy.Labels == nil {
saCopy.Labels = map[string]string{}
}
saCopy.Labels[meta.OwnerPromotionLabel] = meta.OwnerPromotionLabelTrigger
saCopy.Labels[meta.OwnerPromotionLabel] = meta.ValueTrue
return tc.client.Update(context.TODO(), saCopy)
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
@@ -212,7 +212,7 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label("
if saCopy.Labels == nil {
saCopy.Labels = map[string]string{}
}
saCopy.Labels[meta.OwnerPromotionLabel] = meta.OwnerPromotionLabelTrigger
saCopy.Labels[meta.OwnerPromotionLabel] = meta.ValueTrue
return tc.client.Update(context.TODO(), saCopy)
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
@@ -271,7 +271,7 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label("
if saCopy.Labels == nil {
saCopy.Labels = map[string]string{}
}
saCopy.Labels[meta.OwnerPromotionLabel] = meta.OwnerPromotionLabelTrigger
saCopy.Labels[meta.OwnerPromotionLabel] = meta.ValueTrue
return tc.client.Update(context.TODO(), saCopy)
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
@@ -281,7 +281,7 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label("
Eventually(func(g Gomega) []rbacv1.Subject {
crb := &rbacv1.ClusterRoleBinding{}
err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: api.ProvisionerRoleName}, crb)
err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: originConfig.Spec.RBAC.ProvisionerClusterRole}, crb)
g.Expect(err).NotTo(HaveOccurred())
return crb.Subjects
@@ -337,7 +337,7 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label("
Eventually(func(g Gomega) []rbacv1.Subject {
crb := &rbacv1.ClusterRoleBinding{}
err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: api.ProvisionerRoleName}, crb)
err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: originConfig.Spec.RBAC.ProvisionerClusterRole}, crb)
g.Expect(err).NotTo(HaveOccurred())
return crb.Subjects

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