mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-03-17 00:50:31 +00:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd5b3df95a | ||
|
|
189def747d | ||
|
|
634ed49694 | ||
|
|
1267602a1b | ||
|
|
009b34b78e | ||
|
|
b5bdc75a63 | ||
|
|
135077aef8 | ||
|
|
9537c06ee4 | ||
|
|
63eb807cec | ||
|
|
7e9719ac5e | ||
|
|
375062f3e9 | ||
|
|
1fa7ab03c9 | ||
|
|
628c2cefbe | ||
|
|
7dc2538d9b | ||
|
|
b531a8628d | ||
|
|
b8dd20c328 | ||
|
|
b2910990a2 | ||
|
|
8867f9722c | ||
|
|
2261ea6f4e | ||
|
|
d1e0ac5be6 | ||
|
|
ba15a83f94 | ||
|
|
40d17bcdba | ||
|
|
0863915307 | ||
|
|
97f05c062c | ||
|
|
66d304ab92 | ||
|
|
5d07cc29a4 | ||
|
|
deb4db72a1 | ||
|
|
51518679f6 | ||
|
|
c7b672cde5 | ||
|
|
e7da3b080a | ||
|
|
800d49c7f8 | ||
|
|
d342fad60f | ||
|
|
beafe09f71 | ||
|
|
ea2b6ec1e3 | ||
|
|
7ccb64dc47 | ||
|
|
e6de39d920 | ||
|
|
b1d0f8b441 | ||
|
|
a5e79a43b5 | ||
|
|
89e8da3ac9 | ||
|
|
66b3c6971c | ||
|
|
1e8cf5dc1f | ||
|
|
f8f237d585 | ||
|
|
c901412df1 | ||
|
|
d865df2b2b | ||
|
|
ef83abdfe8 | ||
|
|
8254c55848 | ||
|
|
14e09ead3c | ||
|
|
5ac0f83c5a | ||
|
|
9a2effd74e | ||
|
|
b8f7d5a227 | ||
|
|
3b6ac1f377 | ||
|
|
e983c51a0a | ||
|
|
ef63830907 | ||
|
|
4878e1ab1f | ||
|
|
611a7eba8e | ||
|
|
bae5d23ccb | ||
|
|
9bd18d5f08 | ||
|
|
b88f21478c | ||
|
|
72a6148896 | ||
|
|
9965b6ce70 | ||
|
|
bdf34ee026 | ||
|
|
d271031b7c | ||
|
|
3a6de640bf | ||
|
|
7793f5a8a1 | ||
|
|
1942dd4835 | ||
|
|
dd70ac2b9f | ||
|
|
9fa1abac65 | ||
|
|
a2e4e00724 | ||
|
|
ee5c8f02ed | ||
|
|
7542ebda5e | ||
|
|
e2418ab095 | ||
|
|
b9dc782c47 | ||
|
|
d7097b5750 | ||
|
|
2c210ae4db | ||
|
|
54e80f8df1 | ||
|
|
7d617aee47 | ||
|
|
bb8a5110ec | ||
|
|
6e0cae7185 | ||
|
|
c65a142e83 | ||
|
|
f60e52d633 |
4
.github/actions/setup-caches/action.yaml
vendored
4
.github/actions/setup-caches/action.yaml
vendored
@@ -9,11 +9,11 @@ inputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-pkg-mod-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
|
||||
- uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
if: ${{ inputs.build-cache-key }}
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
|
||||
2
.github/workflows/check-actions.yml
vendored
2
.github/workflows/check-actions.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Ensure SHA pinned actions
|
||||
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@fc87bb5b5a97953d987372e74478de634726b3e5 # v3.0.25
|
||||
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@9e9574ef04ea69da568d6249bd69539ccc704e74 # v4.0.0
|
||||
with:
|
||||
# slsa-github-generator requires using a semver tag for reusable workflows.
|
||||
# See: https://github.com/slsa-framework/slsa-github-generator#referencing-slsa-builders-and-generators
|
||||
|
||||
2
.github/workflows/check-pr.yml
vendored
2
.github/workflows/check-pr.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@e7d011b07ef37e089bea6539210f6a0d360d8af9
|
||||
- uses: amannn/action-semantic-pull-request@e49f57ce06c1747542fce2243c7a98682384bc0e
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
6
.github/workflows/coverage.yml
vendored
6
.github/workflows/coverage.yml
vendored
@@ -52,11 +52,11 @@ jobs:
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- name: Run Gosec Security Scanner
|
||||
uses: securego/gosec@c9453023c4e81ebdb6dde29e22d9cd5e2285fb16 # v2.22.8
|
||||
uses: securego/gosec@6be2b51fd78feca86af91f5186b7964d76cb1256 # v2.22.10
|
||||
with:
|
||||
args: '-no-fail -fmt sarif -out gosec.sarif ./...'
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@1fd8a71a1271a5ad7639a423fdd9485e1be64031
|
||||
uses: github/codeql-action/upload-sarif@ae78991f558bb4195cb8a727cb6679c362b9cf24
|
||||
with:
|
||||
sarif_file: gosec.sarif
|
||||
unit_tests:
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
value: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Upload Report to Codecov
|
||||
if: ${{ steps.checksecret.outputs.result == 'true' }}
|
||||
uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: projectcapsule/capsule
|
||||
|
||||
2
.github/workflows/docker-build.yml
vendored
2
.github/workflows/docker-build.yml
vendored
@@ -40,6 +40,6 @@ jobs:
|
||||
# See: https://github.com/aquasecurity/trivy-action/issues/389#issuecomment-2385416577
|
||||
TRIVY_DB_REPOSITORY: 'public.ecr.aws/aquasecurity/trivy-db:2'
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@1fd8a71a1271a5ad7639a423fdd9485e1be64031
|
||||
uses: github/codeql-action/upload-sarif@ae78991f558bb4195cb8a727cb6679c362b9cf24
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
|
||||
2
.github/workflows/docker-publish.yml
vendored
2
.github/workflows/docker-publish.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
output: 'trivy-results.sarif'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||
uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
|
||||
- name: Publish Capsule
|
||||
id: publish-capsule
|
||||
uses: peak-scale/github-actions/make-ko-publish@a441cca016861c546ab7e065277e40ce41a3eb84 # v0.2.0
|
||||
|
||||
2
.github/workflows/e2e.yml
vendored
2
.github/workflows/e2e.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4
|
||||
- uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
|
||||
with:
|
||||
version: v3.14.2
|
||||
- name: e2e
|
||||
|
||||
2
.github/workflows/helm-publish.yml
vendored
2
.github/workflows/helm-publish.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
chart-digest: ${{ steps.helm_publish.outputs.digest }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||
- uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
|
||||
- name: "Extract Version"
|
||||
id: extract_version
|
||||
run: |
|
||||
|
||||
2
.github/workflows/helm-test.yml
vendored
2
.github/workflows/helm-test.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4
|
||||
- uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
|
||||
- name: Linting Chart
|
||||
run: helm lint ./charts/capsule
|
||||
|
||||
|
||||
4
.github/workflows/releaser.yml
vendored
4
.github/workflows/releaser.yml
vendored
@@ -30,9 +30,9 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
continue-on-error: true
|
||||
- uses: creekorful/goreportcard-action@1f35ced8cdac2cba28c9a2f2288a16aacfd507f9 # v1.0
|
||||
- uses: anchore/sbom-action/download-syft@da167eac915b4e86f08b264dbdbc867b61be6f0c
|
||||
- uses: anchore/sbom-action/download-syft@8e94d75ddd33f69f691467e42275782e4bfefe84
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||
uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
||||
with:
|
||||
|
||||
6
.github/workflows/scorecard.yml
vendored
6
.github/workflows/scorecard.yml
vendored
@@ -24,19 +24,19 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run analysis
|
||||
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
|
||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
repo_token: ${{ secrets.SCORECARD_READ_TOKEN }}
|
||||
publish_results: true
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
- name: Upload to code-scanning
|
||||
uses: github/codeql-action/upload-sarif@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0
|
||||
uses: github/codeql-action/upload-sarif@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Close stale pull requests
|
||||
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f
|
||||
uses: actions/stale@e46bbabb3ede15841d25946157759558dd16306e
|
||||
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.'
|
||||
|
||||
@@ -5,6 +5,7 @@ run:
|
||||
linters:
|
||||
default: all
|
||||
disable:
|
||||
- godoclint
|
||||
- depguard
|
||||
- err113
|
||||
- exhaustruct
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
|
||||
rev: v9.22.0
|
||||
rev: v9.23.0
|
||||
hooks:
|
||||
- id: commitlint
|
||||
stages: [commit-msg]
|
||||
|
||||
69
ADOPTERS.md
69
ADOPTERS.md
@@ -7,40 +7,43 @@ This is a list of companies that have adopted Capsule, feel free to open a Pull-
|
||||
## Adopters list (alphabetically)
|
||||
|
||||
### [Bedag Informatik AG](https://www.bedag.ch/)
|
||||

|
||||
<img src="https://www.bedag.ch/wGlobal/wGlobal/layout/images/logo.svg" alt="Bedag" width="350" />
|
||||
|
||||
### [Department of Defense](https://www.defense.gov/)
|
||||

|
||||
|
||||
### [KubeRocketCI](https://docs.kuberocketci.io/)
|
||||

|
||||
|
||||
### [Fastweb](https://www.fastweb.it/)
|
||||

|
||||
|
||||
### [Klarrio](https://klarrio.com/)
|
||||

|
||||
|
||||
### [PITS Global Data Recovery Services](https://www.pitsdatarecovery.net)
|
||||

|
||||
|
||||
### [Politecnico di Torino](https://www.polito.it/)
|
||||

|
||||
|
||||
### [Reevo](https://www.reevo.it/)
|
||||

|
||||
|
||||
### [Seeweb](https://seeweb.it/en)
|
||||

|
||||
|
||||
### [University of Torino](https://www.unito.it)
|
||||

|
||||
|
||||
### [Velocity](https://velocity.tech/)
|
||||

|
||||
|
||||
### [Wargaming.net](https://www.wargaming.net/)
|
||||

|
||||
<img src="https://www.access-board.gov/images/dod-seal.png" alt="United States Department of Defense" width="350" />
|
||||
|
||||
### [Enreach](https://www.enreach.com/)
|
||||

|
||||
<img src="https://campaigns.enreach.com/hubfs/Global/logos/Enreach-logo-vertical-indigo.svg" alt="Enreach" width="350" />
|
||||
|
||||
### [Fastweb](https://www.fastweb.it/)
|
||||
<img src="https://www.fastweb.it/var/storage_feeds/CMS-Company/articoli/0c2/0c252987b90a18017dedf2ed9feda129/640x360.jpg" alt="Fastweb" width="350" />
|
||||
|
||||
### [Klarrio](https://klarrio.com/)
|
||||
<img src="https://klarrio.com/wp-content/uploads/klarrio.png" alt="Klarrio" width="350" />
|
||||
|
||||
### [KubeRocketCI](https://docs.kuberocketci.io/)
|
||||
<img src="https://raw.githubusercontent.com/epam/edp-install/master/docs/assets/krci-logo-267×150-white.png" alt="KubeRocketCI" width="350" />
|
||||
|
||||
### [ODC-Noord](https://odc-noord.nl/)
|
||||
<img src="./assets/customer_logo/odc-noord-logo.png" alt="ODC-Noord" width="350" />
|
||||
|
||||
### [PITS Global Data Recovery Services](https://www.pitsdatarecovery.net)
|
||||
<img src="https://www.pitsdatarecovery.net/wp-content/uploads/2020/09/pits-logo.svg" alt="PITS Global Data Recovery Services" width="350" />
|
||||
|
||||
### [Politecnico di Torino](https://www.polito.it/)
|
||||
<img src="https://www.polito.it/themes/custom/polito/polito_logo_desktop.svg" alt="Politecnico di Torino" width="350" />
|
||||
|
||||
### [Reevo](https://www.reevo.it/)
|
||||
<img src="https://www.reevo.it/hs-fs/hubfs/logo_reevo_azzurro.png" alt="Reevo Cloud and CyberSecurity" width="350" />
|
||||
|
||||
### [Seeweb](https://seeweb.it/en)
|
||||
<img src="https://www.seeweb.it/assets/images/logo-seeweb.svg" alt="Seeweb x Serverless GPU" width="350" />
|
||||
|
||||
### [University of Torino](https://www.unito.it)
|
||||
<img src="https://www.unito.it/sites/all/themes/bsunito/img/logo_new_2022.svg" alt="University of Torino" width="350" />
|
||||
|
||||
### [Velocity](https://velocity.tech/)
|
||||
<img src="https://raw.githubusercontent.com/yarelm/velocity-logo/main/velocity.png" alt="Velocity" width="350" />
|
||||
|
||||
### [Wargaming.net](https://www.wargaming.net/)
|
||||
<img src="https://download.logo.wine/logo/Wargaming_%28company%29/Wargaming_%28company%29-Logo.wine.png" alt="Wargaming.net" width="350" />
|
||||
|
||||
12
Makefile
12
Makefile
@@ -19,7 +19,7 @@ CAPSULE_IMG ?= $(REGISTRY)/$(IMG_BASE)
|
||||
CLUSTER_NAME ?= capsule
|
||||
|
||||
## Kubernetes Version Support
|
||||
KUBERNETES_SUPPORTED_VERSION ?= "v1.33.0"
|
||||
KUBERNETES_SUPPORTED_VERSION ?= "v1.34.0"
|
||||
|
||||
## Tool Binaries
|
||||
KUBECTL ?= kubectl
|
||||
@@ -151,6 +151,7 @@ dev-setup:
|
||||
--create-namespace \
|
||||
--set 'crds.install=true' \
|
||||
--set 'crds.exclusive=true'\
|
||||
--set 'crds.createConfig=true'\
|
||||
--set "webhooks.exclusive=true"\
|
||||
--set "webhooks.service.url=$${WEBHOOK_URL}" \
|
||||
--set "webhooks.service.caBundle=$${CA_BUNDLE}" \
|
||||
@@ -259,7 +260,8 @@ e2e-install: ko-build-all
|
||||
--set 'manager.resources=null'\
|
||||
--set "manager.image.tag=$(VERSION)" \
|
||||
--set 'manager.livenessProbe.failureThreshold=10' \
|
||||
--set 'manager.readinessProbe.failureThreshold=10' \
|
||||
--set 'webhooks.hooks.nodes.enabled=true' \
|
||||
--set "webhooks.exclusive=true"\
|
||||
capsule \
|
||||
./charts/capsule
|
||||
|
||||
@@ -355,7 +357,7 @@ ginkgo:
|
||||
$(call go-install-tool,$(GINKGO),github.com/onsi/ginkgo/v2/ginkgo)
|
||||
|
||||
CT := $(LOCALBIN)/ct
|
||||
CT_VERSION := v3.13.0
|
||||
CT_VERSION := v3.14.0
|
||||
CT_LOOKUP := helm/chart-testing
|
||||
ct:
|
||||
@test -s $(CT) && $(CT) version | grep -q $(CT_VERSION) || \
|
||||
@@ -376,14 +378,14 @@ ko:
|
||||
$(call go-install-tool,$(KO),github.com/$(KO_LOOKUP)@$(KO_VERSION))
|
||||
|
||||
NWA := $(LOCALBIN)/nwa
|
||||
NWA_VERSION := v0.7.5
|
||||
NWA_VERSION := v0.7.7
|
||||
NWA_LOOKUP := B1NARY-GR0UP/nwa
|
||||
nwa:
|
||||
@test -s $(NWA) && $(NWA) -h | grep -q $(NWA_VERSION) || \
|
||||
$(call go-install-tool,$(NWA),github.com/$(NWA_LOOKUP)@$(NWA_VERSION))
|
||||
|
||||
GOLANGCI_LINT := $(LOCALBIN)/golangci-lint
|
||||
GOLANGCI_LINT_VERSION := v2.4.0
|
||||
GOLANGCI_LINT_VERSION := v2.5.0
|
||||
GOLANGCI_LINT_LOOKUP := golangci/golangci-lint
|
||||
golangci-lint: ## Download golangci-lint locally if necessary.
|
||||
@test -s $(GOLANGCI_LINT) && $(GOLANGCI_LINT) -h | grep -q $(GOLANGCI_LINT_VERSION) || \
|
||||
|
||||
@@ -19,7 +19,7 @@ func (in OwnerListSpec) FindOwner(name string, kind OwnerKind) (owner OwnerSpec)
|
||||
return in[i]
|
||||
}
|
||||
|
||||
return
|
||||
return owner
|
||||
}
|
||||
|
||||
type ByKindAndName OwnerListSpec
|
||||
|
||||
@@ -78,5 +78,5 @@ func (in *Tenant) GetNamespaces() (res []string) {
|
||||
|
||||
res = append(res, in.Status.Namespaces...)
|
||||
|
||||
return
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ type CapsuleConfigurationSpec struct {
|
||||
// Define groups which when found in the request of a user will be ignored by the Capsule
|
||||
// this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups.
|
||||
IgnoreUserWithGroups []string `json:"ignoreUserWithGroups,omitempty"`
|
||||
// ServiceAccounts within tenant namespaces can be promoted to owners of the given tenant
|
||||
// this can be achieved by labeling the serviceaccount and then they are considered owners. This can only be done by other owners of the tenant.
|
||||
// However ServiceAccounts which have been promoted to owner can not promote further serviceAccounts.
|
||||
// +kubebuilder:default=false
|
||||
AllowServiceAccountPromotion bool `json:"allowServiceAccountPromotion,omitempty"`
|
||||
// Enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix,
|
||||
// separated by a dash. This is useful to avoid Namespace name collision in a public CaaS environment.
|
||||
// +kubebuilder:default=false
|
||||
|
||||
@@ -12,6 +12,7 @@ type NamespaceOptions struct {
|
||||
// Specifies the maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
|
||||
Quota *int32 `json:"quota,omitempty"`
|
||||
// Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
|
||||
// Deprecated: Use additionalMetadataList instead
|
||||
AdditionalMetadata *api.AdditionalMetadataSpec `json:"additionalMetadata,omitempty"`
|
||||
// Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant via a list. Optional.
|
||||
AdditionalMetadataList []api.AdditionalMetadataSelectorSpec `json:"additionalMetadataList,omitempty"`
|
||||
@@ -19,4 +20,7 @@ type NamespaceOptions struct {
|
||||
ForbiddenLabels api.ForbiddenListSpec `json:"forbiddenLabels,omitempty"`
|
||||
// Define the annotations that a Tenant Owner cannot set for their Namespace resources.
|
||||
ForbiddenAnnotations api.ForbiddenListSpec `json:"forbiddenAnnotations,omitempty"`
|
||||
// If enabled only metadata from additionalMetadata is reconciled to the namespaces.
|
||||
//+kubebuilder:default:=false
|
||||
ManagedMetadataOnly bool `json:"managedMetadataOnly,omitempty"`
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ type OwnerSpec struct {
|
||||
ClusterRoles []string `json:"clusterRoles,omitempty"`
|
||||
// Proxy settings for tenant owner.
|
||||
ProxyOperations []ProxySettings `json:"proxySettings,omitempty"`
|
||||
// Additional Labels for the synchronized rolebindings
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
// Additional Annotations for the synchronized rolebindings
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:validation:Enum=User;Group;ServiceAccount
|
||||
|
||||
@@ -19,7 +19,7 @@ func (o OwnerListSpec) FindOwner(name string, kind OwnerKind) (owner OwnerSpec)
|
||||
return o[i]
|
||||
}
|
||||
|
||||
return
|
||||
return owner
|
||||
}
|
||||
|
||||
type ByKindAndName OwnerListSpec
|
||||
|
||||
@@ -247,7 +247,7 @@ func (r *ResourcePool) GetNamespaceClaims(namespace string) (claims map[string]*
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return claims, claimedResources
|
||||
}
|
||||
|
||||
// Calculate usage for each namespace.
|
||||
@@ -272,5 +272,5 @@ func (r *ResourcePool) GetClaimedByNamespaceClaims() (claims map[string]corev1.R
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return claims
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func (in *Tenant) GetSubjectsByClusterRoles(ignoreOwnerKind []OwnerKind) (rolePe
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return rolePerms
|
||||
}
|
||||
|
||||
// Get the permissions for a tenant ordered by groups and users.
|
||||
|
||||
@@ -28,5 +28,5 @@ func GetTypeLabel(t metav1.Object) (label string, err error) {
|
||||
err = fmt.Errorf("type %T is not mapped as Capsule label recognized", v)
|
||||
}
|
||||
|
||||
return
|
||||
return label, err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
|
||||
package v1beta2
|
||||
|
||||
import (
|
||||
k8stypes "k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
)
|
||||
|
||||
// +kubebuilder:validation:Enum=Cordoned;Active
|
||||
type tenantState string
|
||||
|
||||
@@ -18,6 +24,68 @@ type TenantStatus struct {
|
||||
State tenantState `json:"state"`
|
||||
// How many namespaces are assigned to the Tenant.
|
||||
Size uint `json:"size"`
|
||||
// List of namespaces assigned to the Tenant.
|
||||
// List of namespaces assigned to the Tenant. (Deprecated)
|
||||
Namespaces []string `json:"namespaces,omitempty"`
|
||||
// Tracks state for the namespaces associated with this tenant
|
||||
Spaces []*TenantStatusNamespaceItem `json:"spaces,omitempty"`
|
||||
// Tenant Condition
|
||||
Conditions meta.ConditionList `json:"conditions"`
|
||||
}
|
||||
|
||||
type TenantStatusNamespaceItem struct {
|
||||
// Conditions
|
||||
Conditions meta.ConditionList `json:"conditions"`
|
||||
// Namespace Name
|
||||
Name string `json:"name"`
|
||||
// Namespace UID
|
||||
UID k8stypes.UID `json:"uid,omitempty"`
|
||||
// Managed Metadata
|
||||
Metadata *TenantStatusNamespaceMetadata `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type TenantStatusNamespaceMetadata struct {
|
||||
// Managed Labels
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
// Managed Annotations
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
func (ms *TenantStatus) GetInstance(stat *TenantStatusNamespaceItem) *TenantStatusNamespaceItem {
|
||||
for _, source := range ms.Spaces {
|
||||
if ms.instancequal(source, stat) {
|
||||
return source
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ms *TenantStatus) UpdateInstance(stat *TenantStatusNamespaceItem) {
|
||||
// Check if the tenant is already present in the status
|
||||
for i, source := range ms.Spaces {
|
||||
if ms.instancequal(source, stat) {
|
||||
ms.Spaces[i] = stat
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ms.Spaces = append(ms.Spaces, stat)
|
||||
}
|
||||
|
||||
func (ms *TenantStatus) RemoveInstance(stat *TenantStatusNamespaceItem) {
|
||||
// Filter out the datasource with given UID
|
||||
filter := []*TenantStatusNamespaceItem{}
|
||||
|
||||
for _, source := range ms.Spaces {
|
||||
if !ms.instancequal(source, stat) {
|
||||
filter = append(filter, source)
|
||||
}
|
||||
}
|
||||
|
||||
ms.Spaces = filter
|
||||
}
|
||||
|
||||
func (ms *TenantStatus) instancequal(a, b *TenantStatusNamespaceItem) bool {
|
||||
return a.Name == b.Name
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ import (
|
||||
|
||||
// TenantSpec defines the desired state of Tenant.
|
||||
type TenantSpec struct {
|
||||
// Specifies the owners of the Tenant. Mandatory.
|
||||
Owners OwnerListSpec `json:"owners"`
|
||||
// Specifies the owners of the Tenant.
|
||||
// Optional
|
||||
Owners OwnerListSpec `json:"owners,omitempty"`
|
||||
// Specifies options for the Namespaces, such as additional metadata or maximum number of namespaces allowed for that Tenant. Once the namespace quota assigned to the Tenant has been reached, the Tenant owner cannot create further namespaces. Optional.
|
||||
NamespaceOptions *NamespaceOptions `json:"namespaceOptions,omitempty"`
|
||||
// Specifies options for the Service, such as additional metadata or block of certain type of Services. Optional.
|
||||
@@ -31,8 +32,10 @@ type TenantSpec struct {
|
||||
// Specifies the label to control the placement of pods on a given pool of worker nodes. All namespaces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
|
||||
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
|
||||
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
|
||||
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
|
||||
NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitempty"`
|
||||
// Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
|
||||
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
|
||||
LimitRanges api.LimitRangesSpec `json:"limitRanges,omitempty"`
|
||||
// Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional.
|
||||
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
|
||||
@@ -73,12 +76,13 @@ type TenantSpec struct {
|
||||
// +kubebuilder:storageversion
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:scope=Cluster,shortName=tnt
|
||||
// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.state",description="The actual state of the Tenant"
|
||||
// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.conditions[?(@.type==\"Cordoned\")].reason",description="The actual state of the Tenant"
|
||||
// +kubebuilder:printcolumn:name="Namespace quota",type="integer",JSONPath=".spec.namespaceOptions.quota",description="The max amount of Namespaces can be created"
|
||||
// +kubebuilder:printcolumn:name="Namespace count",type="integer",JSONPath=".status.size",description="The total amount of Namespaces in use"
|
||||
// +kubebuilder:printcolumn:name="Node selector",type="string",JSONPath=".spec.nodeSelector",description="Node Selector applied to Pods"
|
||||
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Reconcile Status for the tenant"
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="Reconcile Message for the tenant"
|
||||
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age"
|
||||
|
||||
// Tenant is the Schema for the tenants API.
|
||||
type Tenant struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
@@ -93,7 +97,7 @@ func (in *Tenant) GetNamespaces() (res []string) {
|
||||
|
||||
res = append(res, in.Status.Namespaces...)
|
||||
|
||||
return
|
||||
return res
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
@@ -9,6 +9,7 @@ package v1beta2
|
||||
|
||||
import (
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -461,6 +462,20 @@ func (in *OwnerSpec) DeepCopyInto(out *OwnerSpec) {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Annotations != nil {
|
||||
in, out := &in.Annotations, &out.Annotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OwnerSpec.
|
||||
@@ -1215,6 +1230,24 @@ func (in *TenantStatus) DeepCopyInto(out *TenantStatus) {
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Spaces != nil {
|
||||
in, out := &in.Spaces, &out.Spaces
|
||||
*out = make([]*TenantStatusNamespaceItem, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = new(TenantStatusNamespaceItem)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
}
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make(meta.ConditionList, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantStatus.
|
||||
@@ -1226,3 +1259,59 @@ func (in *TenantStatus) DeepCopy() *TenantStatus {
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *TenantStatusNamespaceItem) DeepCopyInto(out *TenantStatusNamespaceItem) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make(meta.ConditionList, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Metadata != nil {
|
||||
in, out := &in.Metadata, &out.Metadata
|
||||
*out = new(TenantStatusNamespaceMetadata)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantStatusNamespaceItem.
|
||||
func (in *TenantStatusNamespaceItem) DeepCopy() *TenantStatusNamespaceItem {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(TenantStatusNamespaceItem)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *TenantStatusNamespaceMetadata) DeepCopyInto(out *TenantStatusNamespaceMetadata) {
|
||||
*out = *in
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Annotations != nil {
|
||||
in, out := &in.Annotations, &out.Annotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantStatusNamespaceMetadata.
|
||||
func (in *TenantStatusNamespaceMetadata) DeepCopy() *TenantStatusNamespaceMetadata {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(TenantStatusNamespaceMetadata)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
BIN
assets/customer_logo/odc-noord-logo.png
Normal file
BIN
assets/customer_logo/odc-noord-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -1,6 +1,8 @@
|
||||
# Deploying the Capsule Operator
|
||||
|
||||
Use the Capsule Operator for easily implementing, managing, and maintaining multitenancy and access control in Kubernetes.
|
||||
Use the Capsule Operator for easily implementing, managing, and maintaining multitenancy and access control in Kubernetes. Please read our installation guide:
|
||||
|
||||
* [https://projectcapsule.dev/docs/operating/setup/installation/](https://projectcapsule.dev/docs/operating/setup/installation/)
|
||||
|
||||
## Major Changes
|
||||
|
||||
@@ -26,7 +28,9 @@ The following Values have changed key or Value:
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| crds.annnotations | object | `{}` | Extra Annotations for CRDs |
|
||||
| crds.createConfig | bool | `false` | Create additionally CapsuleConfiguration even if CRDs are exclusive |
|
||||
| crds.exclusive | bool | `false` | Only install the CRDs, no other primitives |
|
||||
| crds.inline | bool | `false` | |
|
||||
| crds.install | bool | `true` | Install the CustomResourceDefinitions (This also manages the lifecycle of the CRDs for update operations) |
|
||||
| crds.labels | object | `{}` | Extra Labels for CRDs |
|
||||
|
||||
@@ -54,6 +58,8 @@ The following Values have changed key or Value:
|
||||
| global.jobs.kubectl.tolerations | list | `[]` | Set list of tolerations |
|
||||
| global.jobs.kubectl.topologySpreadConstraints | list | `[]` | Set Topology Spread Constraints |
|
||||
| global.jobs.kubectl.ttlSecondsAfterFinished | int | `60` | Sets the ttl in seconds after a finished certgen job is deleted. Set to -1 to never delete. |
|
||||
| global.jobs.postInstall.enabled | bool | `true` | Enable Post Install Job |
|
||||
| global.jobs.preDelete.enabled | bool | `true` | Enable Pre Delete Job |
|
||||
|
||||
### General Parameters
|
||||
|
||||
@@ -64,6 +70,7 @@ The following Values have changed key or Value:
|
||||
| certManager.generateCertificates | bool | `false` | 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 |
|
||||
| imagePullSecrets | list | `[]` | Configuration for `imagePullSecrets` so that you can use a private images registry. |
|
||||
| jobs | object | `{}` | Deprecated, use .global.jobs.kubectl instead |
|
||||
| nodeSelector | object | `{}` | Set the node selector for the Capsule pod |
|
||||
@@ -105,15 +112,20 @@ The following Values have changed key or Value:
|
||||
| manager.image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. |
|
||||
| manager.kind | string | `"Deployment"` | Set the controller deployment mode as `Deployment` or `DaemonSet`. |
|
||||
| manager.livenessProbe | object | `{"httpGet":{"path":"/healthz","port":10080}}` | Configure the liveness probe using Deployment probe spec |
|
||||
| manager.options.allowServiceAccountPromotion | bool | `false` | ServiceAccounts within tenant namespaces can be promoted to owners of the given tenant this can be achieved by labeling the serviceaccount and then they are considered owners. This can only be done by other owners of the tenant. However ServiceAccounts which have been promoted to owner can not promote further serviceAccounts. |
|
||||
| manager.options.annotations | object | `{}` | Additional annotations to add to the CapsuleConfiguration resource |
|
||||
| manager.options.capsuleConfiguration | string | `"default"` | Change the default name of the capsule configuration name |
|
||||
| manager.options.capsuleUserGroups | list | `["projectcapsule.dev"]` | Names of the groups considered as Capsule users. |
|
||||
| manager.options.createConfiguration | bool | `true` | Create Configuration |
|
||||
| manager.options.forceTenantPrefix | bool | `false` | Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash |
|
||||
| manager.options.generateCertificates | bool | `true` | Specifies whether capsule webhooks certificates should be generated by capsule operator |
|
||||
| manager.options.ignoreUserWithGroups | list | `[]` | Define groups which when found in the request of a user will be ignored by the Capsule this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups. |
|
||||
| manager.options.logLevel | string | `"4"` | Set the log verbosity of the capsule with a value from 1 to 10 |
|
||||
| manager.options.labels | object | `{}` | Additional labels to add to the CapsuleConfiguration resource |
|
||||
| manager.options.logLevel | string | `"3"` | 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.userNames | list | `[]` | Names of the users considered as Capsule users. |
|
||||
| manager.options.workers | int | `1` | Workers (MaxConcurrentReconciles) is the maximum number of concurrent Reconciles which can be run (ALPHA). |
|
||||
| manager.rbac.create | bool | `true` | Specifies whether RBAC resources should be created. |
|
||||
| manager.rbac.existingClusterRoles | list | `[]` | Specifies further cluster roles to be added to the Capsule manager service account. |
|
||||
| manager.rbac.existingRoles | list | `[]` | Specifies further cluster roles to be added to the Capsule manager service account. |
|
||||
@@ -133,7 +145,7 @@ The following Values have changed key or Value:
|
||||
| monitoring.dashboards.labels | object | `{}` | Labels for dashboard configmaps |
|
||||
| monitoring.dashboards.namespace | string | `""` | Custom namespace for dashboard configmaps |
|
||||
| monitoring.dashboards.operator.allowCrossNamespaceImport | bool | `true` | Allow the Operator to match this resource with Grafanas outside the current namespace |
|
||||
| monitoring.dashboards.operator.enabled | bool | `true` | Enable Operator Resources (GrafanaDashboard) |
|
||||
| monitoring.dashboards.operator.enabled | bool | `false` | Enable Operator Resources (GrafanaDashboard) |
|
||||
| monitoring.dashboards.operator.folder | string | `""` | folder assignment for dashboard |
|
||||
| monitoring.dashboards.operator.instanceSelector | object | `{}` | Selects Grafana instances for import |
|
||||
| monitoring.dashboards.operator.resyncPeriod | string | `"10m"` | How often the resource is synced, defaults to 10m0s if not set |
|
||||
@@ -196,7 +208,7 @@ The following Values have changed key or Value:
|
||||
| webhooks.hooks.networkpolicies.matchPolicy | string | `"Equivalent"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
| webhooks.hooks.networkpolicies.namespaceSelector | object | `{"matchExpressions":[{"key":"capsule.clastix.io/tenant","operator":"Exists"}]}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
|
||||
| webhooks.hooks.networkpolicies.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
|
||||
| webhooks.hooks.nodes.enabled | bool | `true` | Enable the Hook |
|
||||
| webhooks.hooks.nodes.enabled | bool | `false` | Enable the Hook |
|
||||
| webhooks.hooks.nodes.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
|
||||
| webhooks.hooks.nodes.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
| webhooks.hooks.nodes.matchPolicy | string | `"Exact"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
@@ -228,6 +240,12 @@ The following Values have changed key or Value:
|
||||
| webhooks.hooks.resourcepools.pools.matchPolicy | string | `"Equivalent"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
| webhooks.hooks.resourcepools.pools.namespaceSelector | object | `{}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
|
||||
| webhooks.hooks.resourcepools.pools.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
|
||||
| webhooks.hooks.serviceaccounts.enabled | bool | `true` | Enable the Hook |
|
||||
| webhooks.hooks.serviceaccounts.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
|
||||
| webhooks.hooks.serviceaccounts.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
| webhooks.hooks.serviceaccounts.matchPolicy | string | `"Exact"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
| webhooks.hooks.serviceaccounts.namespaceSelector | object | `{"matchExpressions":[{"key":"capsule.clastix.io/tenant","operator":"Exists"}]}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
|
||||
| webhooks.hooks.serviceaccounts.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
|
||||
| webhooks.hooks.services.enabled | bool | `true` | Enable the Hook |
|
||||
| webhooks.hooks.services.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
|
||||
| webhooks.hooks.services.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
@@ -246,6 +264,7 @@ The following Values have changed key or Value:
|
||||
| webhooks.hooks.tenants.matchPolicy | string | `"Exact"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
| webhooks.hooks.tenants.namespaceSelector | object | `{}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
|
||||
| webhooks.hooks.tenants.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
|
||||
| webhooks.hooks.tenants.reinvocationPolicy | string | `"Never"` | [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) |
|
||||
| webhooks.mutatingWebhooksTimeoutSeconds | int | `30` | Timeout in seconds for mutating webhooks |
|
||||
| webhooks.service.caBundle | string | `""` | CABundle for the webhook service |
|
||||
| webhooks.service.name | string | `""` | Custom service name for the webhook service |
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Deploying the Capsule Operator
|
||||
|
||||
Use the Capsule Operator for easily implementing, managing, and maintaining multitenancy and access control in Kubernetes.
|
||||
Use the Capsule Operator for easily implementing, managing, and maintaining multitenancy and access control in Kubernetes. Please read our installation guide:
|
||||
|
||||
* [https://projectcapsule.dev/docs/operating/setup/installation/](https://projectcapsule.dev/docs/operating/setup/installation/)
|
||||
|
||||
## Major Changes
|
||||
|
||||
|
||||
8
charts/capsule/ci/extra-values.yaml
Normal file
8
charts/capsule/ci/extra-values.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
# -- Array of additional resources to be created alongside Capsule helm chart
|
||||
extraManifests:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: random-config
|
||||
data:
|
||||
random-value: "{{ randAlphaNum 16 }}"
|
||||
@@ -40,6 +40,13 @@ spec:
|
||||
spec:
|
||||
description: CapsuleConfigurationSpec defines the Capsule configuration.
|
||||
properties:
|
||||
allowServiceAccountPromotion:
|
||||
default: false
|
||||
description: |-
|
||||
ServiceAccounts within tenant namespaces can be promoted to owners of the given tenant
|
||||
this can be achieved by labeling the serviceaccount and then they are considered owners. This can only be done by other owners of the tenant.
|
||||
However ServiceAccounts which have been promoted to owner can not promote further serviceAccounts.
|
||||
type: boolean
|
||||
enableTLSReconciler:
|
||||
default: true
|
||||
description: |-
|
||||
|
||||
@@ -68,8 +68,18 @@ spec:
|
||||
the RoleBinding for the given ClusterRole. Optional.
|
||||
items:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Additional Annotations for the synchronized rolebindings
|
||||
type: object
|
||||
clusterRoleName:
|
||||
type: string
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Additional Labels for the synchronized rolebindings
|
||||
type: object
|
||||
subjects:
|
||||
description: kubebuilder:validation:Minimum=1
|
||||
items:
|
||||
@@ -1041,7 +1051,7 @@ spec:
|
||||
status: {}
|
||||
- additionalPrinterColumns:
|
||||
- description: The actual state of the Tenant
|
||||
jsonPath: .status.state
|
||||
jsonPath: .status.conditions[?(@.type=="Cordoned")].reason
|
||||
name: State
|
||||
type: string
|
||||
- description: The max amount of Namespaces can be created
|
||||
@@ -1056,6 +1066,14 @@ spec:
|
||||
jsonPath: .spec.nodeSelector
|
||||
name: Node selector
|
||||
type: string
|
||||
- description: Reconcile Status for the tenant
|
||||
jsonPath: .status.conditions[?(@.type=="Ready")].status
|
||||
name: Ready
|
||||
type: string
|
||||
- description: Reconcile Message for the tenant
|
||||
jsonPath: .status.conditions[?(@.type=="Ready")].message
|
||||
name: Status
|
||||
type: string
|
||||
- description: Age
|
||||
jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
@@ -1091,8 +1109,18 @@ spec:
|
||||
the RoleBinding for the given ClusterRole. Optional.
|
||||
items:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Additional Annotations for the synchronized rolebindings
|
||||
type: object
|
||||
clusterRoleName:
|
||||
type: string
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Additional Labels for the synchronized rolebindings
|
||||
type: object
|
||||
subjects:
|
||||
description: kubebuilder:validation:Minimum=1
|
||||
items:
|
||||
@@ -1319,9 +1347,9 @@ spec:
|
||||
type: string
|
||||
type: object
|
||||
limitRanges:
|
||||
description: Specifies the resource min/max usage restrictions to
|
||||
the Tenant. The assigned values are inherited by any namespace created
|
||||
in the Tenant. Optional.
|
||||
description: |-
|
||||
Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
|
||||
Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
@@ -1410,8 +1438,9 @@ spec:
|
||||
the Tenant owner cannot create further namespaces. Optional.
|
||||
properties:
|
||||
additionalMetadata:
|
||||
description: Specifies additional labels and annotations the Capsule
|
||||
operator places on any Namespace resource in the Tenant. Optional.
|
||||
description: |-
|
||||
Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional.
|
||||
Deprecated: Use additionalMetadataList instead
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
@@ -1509,6 +1538,11 @@ spec:
|
||||
deniedRegex:
|
||||
type: string
|
||||
type: object
|
||||
managedMetadataOnly:
|
||||
default: false
|
||||
description: If enabled only metadata from additionalMetadata
|
||||
is reconciled to the namespaces.
|
||||
type: boolean
|
||||
quota:
|
||||
description: Specifies the maximum number of namespaces allowed
|
||||
for that Tenant. Once the namespace quota assigned to the Tenant
|
||||
@@ -1519,9 +1553,9 @@ spec:
|
||||
type: integer
|
||||
type: object
|
||||
networkPolicies:
|
||||
description: Specifies the NetworkPolicies assigned to the Tenant.
|
||||
The assigned NetworkPolicies are inherited by any namespace created
|
||||
in the Tenant. Optional.
|
||||
description: |-
|
||||
Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
|
||||
Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
@@ -2007,9 +2041,16 @@ spec:
|
||||
label. Optional.
|
||||
type: object
|
||||
owners:
|
||||
description: Specifies the owners of the Tenant. Mandatory.
|
||||
description: |-
|
||||
Specifies the owners of the Tenant.
|
||||
Optional
|
||||
items:
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Additional Annotations for the synchronized rolebindings
|
||||
type: object
|
||||
clusterRoles:
|
||||
default:
|
||||
- admin
|
||||
@@ -2027,6 +2068,11 @@ spec:
|
||||
- Group
|
||||
- ServiceAccount
|
||||
type: string
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Additional Labels for the synchronized rolebindings
|
||||
type: object
|
||||
name:
|
||||
description: Name of tenant owner.
|
||||
type: string
|
||||
@@ -2417,20 +2463,163 @@ spec:
|
||||
type: object
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
required:
|
||||
- owners
|
||||
type: object
|
||||
status:
|
||||
description: Returns the observed state of the Tenant.
|
||||
properties:
|
||||
conditions:
|
||||
description: Tenant Condition
|
||||
items:
|
||||
description: Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False, Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
namespaces:
|
||||
description: List of namespaces assigned to the Tenant.
|
||||
description: List of namespaces assigned to the Tenant. (Deprecated)
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
size:
|
||||
description: How many namespaces are assigned to the Tenant.
|
||||
type: integer
|
||||
spaces:
|
||||
description: Tracks state for the namespaces associated with this
|
||||
tenant
|
||||
items:
|
||||
properties:
|
||||
conditions:
|
||||
description: Conditions
|
||||
items:
|
||||
description: Condition contains details for one aspect of
|
||||
the current state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False,
|
||||
Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
metadata:
|
||||
description: Managed Metadata
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Managed Annotations
|
||||
type: object
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Managed Labels
|
||||
type: object
|
||||
type: object
|
||||
name:
|
||||
description: Namespace Name
|
||||
type: string
|
||||
uid:
|
||||
description: Namespace UID
|
||||
type: string
|
||||
required:
|
||||
- conditions
|
||||
- name
|
||||
type: object
|
||||
type: array
|
||||
state:
|
||||
default: Active
|
||||
description: The operational state of the Tenant. Possible values
|
||||
@@ -2440,6 +2629,7 @@ spec:
|
||||
- Active
|
||||
type: string
|
||||
required:
|
||||
- conditions
|
||||
- size
|
||||
- state
|
||||
type: object
|
||||
|
||||
@@ -53,6 +53,15 @@ app.kubernetes.io/name: {{ include "capsule.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
|
||||
{{/*
|
||||
Release Annotations
|
||||
*/}}
|
||||
{{- define "capsule.releaseAnnotations" -}}
|
||||
meta.helm.sh/release-name: {{ $.Release.Name }}
|
||||
meta.helm.sh/release-namespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
ServiceAccount annotations
|
||||
*/}}
|
||||
@@ -154,3 +163,20 @@ Capsule Webhook endpoint CA Bundle
|
||||
caBundle: {{ $.Values.webhooks.service.caBundle -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
|
||||
{{- define "capsule.crdsSizeHash" -}}
|
||||
{{- $paths := list -}}
|
||||
{{- range $p, $_ := .Files.Glob "crds/**.yaml" }}
|
||||
{{- $paths = append $paths $p -}}
|
||||
{{- end -}}
|
||||
{{- $paths = sortAlpha $paths -}}
|
||||
|
||||
{{- $sizes := list -}}
|
||||
{{- range $paths }}
|
||||
{{- $sizes = append $sizes (len ($.Files.Get .)) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- $joined := join "," $sizes -}}
|
||||
{{- sha256sum $joined -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{{- define "capsule.pod" -}}
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if .Values.crds.install }}
|
||||
projectcapsule.dev/crds-size-hash: {{ include "capsule.crdsSizeHash" . | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
{{- with .Values.podLabels }}
|
||||
@@ -18,7 +21,9 @@ spec:
|
||||
{{- if .Values.podSecurityContext.enabled }}
|
||||
securityContext: {{- omit .Values.podSecurityContext "enabled" | toYaml | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if not .Values.manager.hostUsers }}
|
||||
hostUsers: {{ .Values.manager.hostUsers }}
|
||||
{{- end }}
|
||||
{{- if .Values.manager.hostNetwork }}
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
@@ -59,6 +64,7 @@ spec:
|
||||
- --webhook-port={{ .Values.manager.webhookPort }}
|
||||
- --zap-log-level={{ default 4 .Values.manager.options.logLevel }}
|
||||
- --configuration-name={{ .Values.manager.options.capsuleConfiguration }}
|
||||
- --workers={{ .Values.manager.options.workers }}
|
||||
{{- with .Values.manager.extraArgs }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -69,6 +75,10 @@ spec:
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: SERVICE_ACCOUNT
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.serviceAccountName
|
||||
{{- with .Values.manager.env }}
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -4,6 +4,7 @@ apiVersion: cert-manager.io/v1
|
||||
kind: Issuer
|
||||
metadata:
|
||||
name: {{ include "capsule.fullname" . }}-webhook-selfsigned
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
{{- with .Values.customAnnotations }}
|
||||
@@ -17,6 +18,7 @@ apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: {{ include "capsule.fullname" . }}-webhook-cert
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
{{- with .Values.customAnnotations }}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
{{- with .Values.customAnnotations }}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
{{- if not $.Values.crds.exclusive }}
|
||||
{{- if $.Values.manager.options.createConfiguration }}
|
||||
apiVersion: capsule.clastix.io/v1beta2
|
||||
kind: CapsuleConfiguration
|
||||
metadata:
|
||||
name: default
|
||||
name: {{ .Values.manager.options.capsuleConfiguration }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
{{- with .Values.manager.options.labels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
annotations:
|
||||
{{- with .Values.customAnnotations }}
|
||||
{{- with (mergeOverwrite .Values.customAnnotations .Values.manager.options.annotations) }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
@@ -16,6 +20,7 @@ spec:
|
||||
TLSSecretName: {{ include "capsule.secretTlsName" . }}
|
||||
validatingWebhookConfigurationName: {{ include "capsule.fullname" . }}-validating-webhook-configuration
|
||||
forceTenantPrefix: {{ .Values.manager.options.forceTenantPrefix }}
|
||||
allowServiceAccountPromotion: {{ .Values.manager.options.allowServiceAccountPromotion }}
|
||||
userGroups:
|
||||
{{- toYaml .Values.manager.options.capsuleUserGroups | nindent 4 }}
|
||||
userNames:
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
|
||||
{{/* Add Common Lables */}}
|
||||
{{- $_ := set $p.metadata "annotations" (mergeOverwrite (default dict (get $p.metadata "annotations")) (default dict $.Values.crds.annotations)) -}}
|
||||
{{- $_ := set $p.metadata "annotations" (mergeOverwrite (default dict (get $p.metadata "annotations")) (default dict $.Values.crds.annotations) (fromYaml (include "capsule.releaseAnnotations" $))) -}}
|
||||
|
||||
{{/* Add Keep annotation to CRDs */}}
|
||||
{{- if $.Values.crds.keep }}
|
||||
@@ -33,6 +33,9 @@
|
||||
{{- $p = $tmp -}}
|
||||
{{- end -}}
|
||||
{{- if $p }}
|
||||
{{- if $.Values.crds.inline }}
|
||||
{{- printf "---\n%s" (toYaml $p) | nindent 0 }}
|
||||
{{- else }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
@@ -52,5 +55,6 @@ data:
|
||||
|
||||
{{- end }}
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{/* Backwards compatibility */}}
|
||||
{{- $Values := mergeOverwrite $.Values.global.jobs.kubectl $.Values.jobs -}}
|
||||
|
||||
{{- if .Values.crds.install }}
|
||||
{{- if and .Values.crds.install (not $.Values.crds.inline) }}
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{- if .Values.crds.install }}
|
||||
{{- if and .Values.crds.install (not $.Values.crds.inline) }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
@@ -23,11 +24,19 @@ rules:
|
||||
- apiextensions.k8s.io
|
||||
resources:
|
||||
- customresourcedefinitions
|
||||
resourceNames:
|
||||
- capsuleconfigurations.capsule.clastix.io
|
||||
- resourcepoolclaims.capsule.clastix.io
|
||||
- resourcepools.capsule.clastix.io
|
||||
- tenantresources.capsule.clastix.io
|
||||
- globaltenantresources.capsule.clastix.io
|
||||
- tenants.capsule.clastix.io
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{- if .Values.crds.install }}
|
||||
{{- if and .Values.crds.install (not $.Values.crds.inline) }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
|
||||
@@ -4,6 +4,7 @@ apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: {{ include "capsule.controllerName" . }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
{{- with .Values.customAnnotations }}
|
||||
|
||||
@@ -4,6 +4,7 @@ apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "capsule.controllerName" . }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
{{- with .Values.customAnnotations }}
|
||||
|
||||
4
charts/capsule/templates/extra-manifests.yaml
Normal file
4
charts/capsule/templates/extra-manifests.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
{{ range .Values.extraManifests }}
|
||||
---
|
||||
{{ tpl (toYaml .) $ }}
|
||||
{{ end }}
|
||||
@@ -3,6 +3,7 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "capsule.fullname" . }}-controller-manager-metrics-service
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
{{- with .Values.customAnnotations }}
|
||||
|
||||
@@ -274,4 +274,44 @@ webhooks:
|
||||
timeoutSeconds: {{ $.Values.webhooks.mutatingWebhooksTimeoutSeconds }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with .Values.webhooks.hooks.tenants }}
|
||||
{{- if .enabled }}
|
||||
- name: tenants.projectcapsule.dev
|
||||
admissionReviewVersions:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- include "capsule.webhooks.service" (dict "path" "/tenants/mutating" "ctx" $) | nindent 4 }}
|
||||
failurePolicy: {{ .failurePolicy }}
|
||||
matchPolicy: {{ .matchPolicy }}
|
||||
reinvocationPolicy: {{ .reinvocationPolicy }}
|
||||
{{- with .namespaceSelector }}
|
||||
namespaceSelector:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .objectSelector }}
|
||||
objectSelector:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .matchConditions }}
|
||||
matchConditions:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- capsule.clastix.io
|
||||
apiVersions:
|
||||
- v1beta2
|
||||
operations:
|
||||
- CREATE
|
||||
- UPDATE
|
||||
- DELETE
|
||||
resources:
|
||||
- tenants
|
||||
scope: 'Cluster'
|
||||
sideEffects: None
|
||||
timeoutSeconds: {{ $.Values.webhooks.mutatingWebhooksTimeoutSeconds }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- end }}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
{{- $Values := mergeOverwrite $.Values.global.jobs.kubectl $.Values.jobs -}}
|
||||
|
||||
{{- if .Values.tls.create }}
|
||||
{{- if not $.Values.crds.exclusive }}
|
||||
{{- if and (not $.Values.crds.exclusive) $.Values.global.jobs.postInstall.enabled }}
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: "{{ include "capsule.post-install.name" . }}"
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
app.kubernetes.io/component: {{ include "capsule.post-install.component" . | quote }}
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{- if .Values.tls.create }}
|
||||
{{- if not $.Values.crds.exclusive }}
|
||||
{{- if and (not $.Values.crds.exclusive) $.Values.global.jobs.postInstall.enabled }}
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{- if .Values.tls.create }}
|
||||
{{- if not $.Values.crds.exclusive }}
|
||||
{{- if and (not $.Values.crds.exclusive) $.Values.global.jobs.postInstall.enabled }}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
{{- $Values := mergeOverwrite $.Values.global.jobs.kubectl $.Values.jobs -}}
|
||||
|
||||
{{- if not $.Values.crds.exclusive }}
|
||||
{{- if and (not $.Values.crds.exclusive) $.Values.global.jobs.preDelete.enabled }}
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: "{{ include "capsule.pre-delete.name" $ }}"
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
app.kubernetes.io/component: {{ include "capsule.pre-delete.component" . | quote }}
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{- if not $.Values.crds.exclusive }}
|
||||
{{- if and (not $.Values.crds.exclusive) $.Values.global.jobs.preDelete.enabled }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{- if not $.Values.crds.exclusive }}
|
||||
{{- if and (not $.Values.crds.exclusive) $.Values.global.jobs.preDelete.enabled }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
|
||||
@@ -47,6 +47,7 @@ kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: {{ include "capsule.fullname" $ }}-{{ $nr }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "capsule.labels" $ | nindent 4 }}
|
||||
{{- with $.Values.customAnnotations }}
|
||||
|
||||
@@ -4,6 +4,7 @@ apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "capsule.serviceAccountName" . }}
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
{{- if or (.Values.serviceAccount.annotations) (.Values.customAnnotations) }}
|
||||
|
||||
@@ -3,6 +3,7 @@ apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: ValidatingWebhookConfiguration
|
||||
metadata:
|
||||
name: {{ include "capsule.fullname" . }}-validating-webhook-configuration
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
@@ -261,6 +262,7 @@ webhooks:
|
||||
- UPDATE
|
||||
resources:
|
||||
- pods
|
||||
- pods/ephemeralcontainers
|
||||
scope: Namespaced
|
||||
sideEffects: None
|
||||
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
|
||||
@@ -382,7 +384,7 @@ webhooks:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- include "capsule.webhooks.service" (dict "path" "/tenants" "ctx" $) | nindent 4 }}
|
||||
{{- include "capsule.webhooks.service" (dict "path" "/tenants/validating" "ctx" $) | nindent 4 }}
|
||||
failurePolicy: {{ .failurePolicy }}
|
||||
matchPolicy: {{ .matchPolicy }}
|
||||
{{- with .namespaceSelector }}
|
||||
@@ -408,7 +410,7 @@ webhooks:
|
||||
- DELETE
|
||||
resources:
|
||||
- tenants
|
||||
scope: '*'
|
||||
scope: 'Cluster'
|
||||
sideEffects: None
|
||||
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
|
||||
{{- end }}
|
||||
@@ -480,6 +482,7 @@ webhooks:
|
||||
operations:
|
||||
- CREATE
|
||||
- UPDATE
|
||||
- DELETE
|
||||
resources:
|
||||
- resourcepoolclaims
|
||||
scope: '*'
|
||||
@@ -525,4 +528,41 @@ webhooks:
|
||||
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with .Values.webhooks.hooks.serviceaccounts }}
|
||||
{{- if .enabled }}
|
||||
- name: serviceaccounts.tenant.projectcapsule.dev
|
||||
admissionReviewVersions:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- include "capsule.webhooks.service" (dict "path" "/serviceaccounts" "ctx" $) | nindent 4 }}
|
||||
failurePolicy: {{ .failurePolicy }}
|
||||
matchPolicy: {{ .matchPolicy }}
|
||||
{{- with .namespaceSelector }}
|
||||
namespaceSelector:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .objectSelector }}
|
||||
objectSelector:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .matchConditions }}
|
||||
matchConditions:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- '*'
|
||||
apiVersions:
|
||||
- '*'
|
||||
operations:
|
||||
- CREATE
|
||||
- UPDATE
|
||||
resources:
|
||||
- 'serviceaccounts'
|
||||
scope: Namespaced
|
||||
sideEffects: None
|
||||
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -3,6 +3,7 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "capsule.fullname" . }}-webhook-service
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
{{- with .Values.customAnnotations }}
|
||||
|
||||
@@ -26,10 +26,18 @@
|
||||
"description": "Extra Annotations for CRDs",
|
||||
"type": "object"
|
||||
},
|
||||
"createConfig": {
|
||||
"description": "Create additionally CapsuleConfiguration even if CRDs are exclusive",
|
||||
"type": "boolean"
|
||||
},
|
||||
"exclusive": {
|
||||
"description": "Only install the CRDs, no other primitives",
|
||||
"type": "boolean"
|
||||
},
|
||||
"inline": {
|
||||
"description": "Render CRDS inline (in this case use --skip-crds when installing the chart and create the capsuleconfiguration independently)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"install": {
|
||||
"description": "Install the CustomResourceDefinitions (This also manages the lifecycle of the CRDs for update operations)",
|
||||
"type": "boolean"
|
||||
@@ -48,6 +56,10 @@
|
||||
"description": "Additional labels which will be added to all resources created by Capsule helm chart",
|
||||
"type": "object"
|
||||
},
|
||||
"extraManifests": {
|
||||
"description": "Array of additional resources to be created alongside Capsule helm chart",
|
||||
"type": "array"
|
||||
},
|
||||
"global": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -187,6 +199,24 @@
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"postInstall": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Enable Post Install Job",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"preDelete": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Enable Pre Delete Job",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,6 +319,14 @@
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allowServiceAccountPromotion": {
|
||||
"description": "ServiceAccounts within tenant namespaces can be promoted to owners of the given tenant this can be achieved by labeling the serviceaccount and then they are considered owners. This can only be done by other owners of the tenant. However ServiceAccounts which have been promoted to owner can not promote further serviceAccounts.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"annotations": {
|
||||
"description": "Additional annotations to add to the CapsuleConfiguration resource",
|
||||
"type": "object"
|
||||
},
|
||||
"capsuleConfiguration": {
|
||||
"description": "Change the default name of the capsule configuration name",
|
||||
"type": "string"
|
||||
@@ -300,6 +338,10 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"createConfiguration": {
|
||||
"description": "Create Configuration",
|
||||
"type": "boolean"
|
||||
},
|
||||
"forceTenantPrefix": {
|
||||
"description": "Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash",
|
||||
"type": "boolean"
|
||||
@@ -312,8 +354,12 @@
|
||||
"description": "Define groups which when found in the request of a user will be ignored by the Capsule this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups.",
|
||||
"type": "array"
|
||||
},
|
||||
"labels": {
|
||||
"description": "Additional labels to add to the CapsuleConfiguration resource",
|
||||
"type": "object"
|
||||
},
|
||||
"logLevel": {
|
||||
"description": "Set the log verbosity of the capsule with a value from 1 to 10",
|
||||
"description": "Set the log verbosity of the capsule with a value from 1 to 5",
|
||||
"type": "string"
|
||||
},
|
||||
"nodeMetadata": {
|
||||
@@ -351,6 +397,10 @@
|
||||
"userNames": {
|
||||
"description": "Names of the users considered as Capsule users.",
|
||||
"type": "array"
|
||||
},
|
||||
"workers": {
|
||||
"description": "Workers (MaxConcurrentReconciles) is the maximum number of concurrent Reconciles which can be run (ALPHA).",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1194,6 +1244,51 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"serviceaccounts": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Enable the Hook",
|
||||
"type": "boolean"
|
||||
},
|
||||
"failurePolicy": {
|
||||
"description": "[FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy)",
|
||||
"type": "string"
|
||||
},
|
||||
"matchConditions": {
|
||||
"description": "[MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)",
|
||||
"type": "array"
|
||||
},
|
||||
"matchPolicy": {
|
||||
"description": "[MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)",
|
||||
"type": "string"
|
||||
},
|
||||
"namespaceSelector": {
|
||||
"description": "[NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchExpressions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"operator": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"objectSelector": {
|
||||
"description": "[ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector)",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1326,6 +1421,10 @@
|
||||
"objectSelector": {
|
||||
"description": "[ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector)",
|
||||
"type": "object"
|
||||
},
|
||||
"reinvocationPolicy": {
|
||||
"description": "[ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
|
||||
global:
|
||||
jobs:
|
||||
postInstall:
|
||||
# -- Enable Post Install Job
|
||||
enabled: true
|
||||
preDelete:
|
||||
# -- Enable Pre Delete Job
|
||||
enabled: true
|
||||
kubectl:
|
||||
image:
|
||||
# -- Set the image repository of the helm chart job
|
||||
@@ -65,10 +71,14 @@ crds:
|
||||
install: true
|
||||
# -- Only install the CRDs, no other primitives
|
||||
exclusive: false
|
||||
# -- Create additionally CapsuleConfiguration even if CRDs are exclusive
|
||||
createConfig: false
|
||||
# -- Extra Labels for CRDs
|
||||
labels: {}
|
||||
# -- Extra Annotations for CRDs
|
||||
annnotations: {}
|
||||
# -- Render CRDS inline (in this case use --skip-crds when installing the chart and create the capsuleconfiguration independently)
|
||||
inline: false
|
||||
|
||||
# Secret Options
|
||||
tls:
|
||||
@@ -156,12 +166,18 @@ manager:
|
||||
|
||||
# Additional Capsule Controller Options
|
||||
options:
|
||||
# -- Create Configuration
|
||||
createConfiguration: true
|
||||
# -- Change the default name of the capsule configuration name
|
||||
capsuleConfiguration: default
|
||||
# -- Set the log verbosity of the capsule with a value from 1 to 10
|
||||
logLevel: '4'
|
||||
# -- Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash
|
||||
forceTenantPrefix: false
|
||||
# -- Additional labels to add to the CapsuleConfiguration resource
|
||||
labels: {}
|
||||
# -- Additional annotations to add to the CapsuleConfiguration resource
|
||||
annotations: {}
|
||||
# -- Workers (MaxConcurrentReconciles) is the maximum number of concurrent Reconciles which can be run (ALPHA).
|
||||
workers: 1
|
||||
# -- Set the log verbosity of the capsule with a value from 1 to 5
|
||||
logLevel: '3'
|
||||
# -- Names of the users considered as Capsule users.
|
||||
userNames: []
|
||||
# -- Names of the groups considered as Capsule users.
|
||||
@@ -169,6 +185,12 @@ manager:
|
||||
# -- Define groups which when found in the request of a user will be ignored by the Capsule
|
||||
# this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups.
|
||||
ignoreUserWithGroups: []
|
||||
# -- ServiceAccounts within tenant namespaces can be promoted to owners of the given tenant
|
||||
# this can be achieved by labeling the serviceaccount and then they are considered owners. This can only be done by other owners of the tenant.
|
||||
# However ServiceAccounts which have been promoted to owner can not promote further serviceAccounts.
|
||||
allowServiceAccountPromotion: false
|
||||
# -- Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash
|
||||
forceTenantPrefix: false
|
||||
# -- If specified, disallows creation of namespaces matching the passed regexp
|
||||
protectedNamespaceRegex: ""
|
||||
# -- Specifies whether capsule webhooks certificates should be generated by capsule operator
|
||||
@@ -218,9 +240,6 @@ imagePullSecrets: []
|
||||
|
||||
# -- Labels to add to the capsule pod.
|
||||
podLabels: {}
|
||||
# The following annotations guarantee scheduling for critical add-on pods
|
||||
# podAnnotations:
|
||||
# scheduler.alpha.kubernetes.io/critical-pod: ''
|
||||
|
||||
# -- Annotations to add to the capsule pod.
|
||||
podAnnotations: {}
|
||||
@@ -295,6 +314,15 @@ customLabels: {}
|
||||
# -- Additional annotations which will be added to all resources created by Capsule helm chart
|
||||
customAnnotations: {}
|
||||
|
||||
# -- Array of additional resources to be created alongside Capsule helm chart
|
||||
extraManifests: []
|
||||
# - apiVersion: v1
|
||||
# kind: ConfigMap
|
||||
# metadata:
|
||||
# name: extra-configmap
|
||||
# data:
|
||||
# key: value
|
||||
|
||||
# Monitoring Settings
|
||||
monitoring:
|
||||
|
||||
@@ -311,7 +339,7 @@ monitoring:
|
||||
# Grafana Operator
|
||||
operator:
|
||||
# -- Enable Operator Resources (GrafanaDashboard)
|
||||
enabled: true
|
||||
enabled: false
|
||||
# -- Allow the Operator to match this resource with Grafanas outside the current namespace
|
||||
allowCrossNamespaceImport: true
|
||||
# -- How often the resource is synced, defaults to 10m0s if not set
|
||||
@@ -569,6 +597,8 @@ webhooks:
|
||||
namespaceSelector: {}
|
||||
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
|
||||
matchConditions: []
|
||||
# -- [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)
|
||||
reinvocationPolicy: Never
|
||||
|
||||
tenantResourceObjects:
|
||||
# -- Enable the Hook
|
||||
@@ -609,7 +639,7 @@ webhooks:
|
||||
|
||||
nodes:
|
||||
# -- Enable the Hook
|
||||
enabled: true
|
||||
enabled: false
|
||||
# -- [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy)
|
||||
failurePolicy: Fail
|
||||
# -- [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
|
||||
@@ -621,6 +651,23 @@ webhooks:
|
||||
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
|
||||
matchConditions: []
|
||||
|
||||
serviceaccounts:
|
||||
# -- Enable the Hook
|
||||
enabled: true
|
||||
# -- [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy)
|
||||
failurePolicy: Fail
|
||||
# -- [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
|
||||
matchPolicy: Exact
|
||||
# -- [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector)
|
||||
objectSelector: {}
|
||||
# -- [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector)
|
||||
namespaceSelector:
|
||||
matchExpressions:
|
||||
- key: capsule.clastix.io/tenant
|
||||
operator: Exists
|
||||
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
|
||||
matchConditions: []
|
||||
|
||||
# -- Deprecated, use webhooks.hooks.namespaces instead
|
||||
namespaceOwnerReference: {}
|
||||
|
||||
|
||||
29
cmd/main.go
29
cmd/main.go
@@ -40,6 +40,7 @@ import (
|
||||
servicelabelscontroller "github.com/projectcapsule/capsule/controllers/servicelabels"
|
||||
tenantcontroller "github.com/projectcapsule/capsule/controllers/tenant"
|
||||
tlscontroller "github.com/projectcapsule/capsule/controllers/tls"
|
||||
utilscontroller "github.com/projectcapsule/capsule/controllers/utils"
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
"github.com/projectcapsule/capsule/pkg/indexer"
|
||||
"github.com/projectcapsule/capsule/pkg/metrics"
|
||||
@@ -56,7 +57,9 @@ import (
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/resourcepool"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/route"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/service"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/tenant"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/serviceaccounts"
|
||||
tenantmutation "github.com/projectcapsule/capsule/pkg/webhook/tenant/mutation"
|
||||
tenantvalidation "github.com/projectcapsule/capsule/pkg/webhook/tenant/validation"
|
||||
tntresource "github.com/projectcapsule/capsule/pkg/webhook/tenantresource"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
)
|
||||
@@ -85,6 +88,8 @@ func printVersion() {
|
||||
|
||||
//nolint:maintidx
|
||||
func main() {
|
||||
controllerConfig := utilscontroller.ControllerOptions{}
|
||||
|
||||
var enableLeaderElection, version bool
|
||||
|
||||
var metricsAddr, namespace, configurationName string
|
||||
@@ -93,6 +98,7 @@ func main() {
|
||||
|
||||
var goFlagSet goflag.FlagSet
|
||||
|
||||
flag.IntVar(&controllerConfig.MaxConcurrentReconciles, "workers", 1, "MaxConcurrentReconciles is the maximum number of concurrent Reconciles which can be run.")
|
||||
flag.IntVar(&webhookPort, "webhook-port", 9443, "The port the webhook server binds to.")
|
||||
flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
|
||||
flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
|
||||
@@ -201,7 +207,7 @@ func main() {
|
||||
Metrics: metrics.MustMakeTenantRecorder(),
|
||||
Log: ctrl.Log.WithName("controllers").WithName("Tenant"),
|
||||
Recorder: manager.GetEventRecorderFor("tenant-controller"),
|
||||
}).SetupWithManager(manager); err != nil {
|
||||
}).SetupWithManager(manager, controllerConfig); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "Tenant")
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -226,18 +232,20 @@ func main() {
|
||||
// webhooks: the order matters, don't change it and just append
|
||||
webhooksList := append(
|
||||
make([]webhook.Webhook, 0),
|
||||
route.Pod(pod.ImagePullPolicy(), pod.ContainerRegistry(), pod.PriorityClass(), pod.RuntimeClass()),
|
||||
route.Namespace(utils.InCapsuleGroups(cfg, namespacevalidation.PatchHandler(), namespacevalidation.QuotaHandler(), namespacevalidation.FreezeHandler(cfg), namespacevalidation.PrefixHandler(cfg), namespacevalidation.UserMetadataHandler())),
|
||||
route.Pod(pod.ImagePullPolicy(), pod.ContainerRegistry(cfg), pod.PriorityClass(), pod.RuntimeClass()),
|
||||
route.Namespace(utils.InCapsuleGroups(cfg, namespacevalidation.PatchHandler(cfg), namespacevalidation.QuotaHandler(), namespacevalidation.FreezeHandler(cfg), namespacevalidation.PrefixHandler(cfg), namespacevalidation.UserMetadataHandler())),
|
||||
route.Ingress(ingress.Class(cfg, kubeVersion), ingress.Hostnames(cfg), ingress.Collision(cfg), ingress.Wildcard()),
|
||||
route.PVC(pvc.Validating(), pvc.PersistentVolumeReuse()),
|
||||
route.Service(service.Handler()),
|
||||
route.TenantResourceObjects(utils.InCapsuleGroups(cfg, tntresource.WriteOpsHandler())),
|
||||
route.NetworkPolicy(utils.InCapsuleGroups(cfg, networkpolicy.Handler())),
|
||||
route.Tenant(tenant.NameHandler(), tenant.RoleBindingRegexHandler(), tenant.IngressClassRegexHandler(), tenant.StorageClassRegexHandler(), tenant.ContainerRegistryRegexHandler(), tenant.HostnameRegexHandler(), tenant.FreezedEmitter(), tenant.ServiceAccountNameHandler(), tenant.ForbiddenAnnotationsRegexHandler(), tenant.ProtectedHandler(), tenant.MetaHandler()),
|
||||
route.Cordoning(tenant.CordoningHandler(cfg)),
|
||||
route.TenantMutating(tenantmutation.MetaHandler()),
|
||||
route.TenantValidating(tenantvalidation.NameHandler(), tenantvalidation.RoleBindingRegexHandler(), tenantvalidation.IngressClassRegexHandler(), tenantvalidation.StorageClassRegexHandler(), tenantvalidation.ContainerRegistryRegexHandler(), tenantvalidation.HostnameRegexHandler(), tenantvalidation.FreezedEmitter(), tenantvalidation.ServiceAccountNameHandler(), tenantvalidation.ForbiddenAnnotationsRegexHandler(), tenantvalidation.ProtectedHandler()),
|
||||
route.Cordoning(tenantvalidation.CordoningHandler(cfg)),
|
||||
route.Node(utils.InCapsuleGroups(cfg, node.UserMetadataHandler(cfg, kubeVersion))),
|
||||
route.ServiceAccounts(serviceaccounts.Handler(cfg)),
|
||||
route.NamespacePatch(utils.InCapsuleGroups(cfg, namespacemutation.CordoningLabelHandler(cfg), namespacemutation.OwnerReferenceHandler(cfg), namespacemutation.MetadataHandler(cfg))),
|
||||
route.CustomResources(tenant.ResourceCounterHandler(manager.GetClient())),
|
||||
route.CustomResources(tenantvalidation.ResourceCounterHandler(manager.GetClient())),
|
||||
route.Gateway(gateway.Class(cfg)),
|
||||
route.Defaults(defaults.Handler(cfg, kubeVersion)),
|
||||
route.ResourcePoolMutation((resourcepool.PoolMutationHandler(ctrl.Log.WithName("webhooks").WithName("resourcepool")))),
|
||||
@@ -290,7 +298,7 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&pv.Controller{}).SetupWithManager(manager); err != nil {
|
||||
if err = (&pv.Controller{}).SetupWithManager(manager, controllerConfig); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "PersistentVolume")
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -302,12 +310,12 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&resources.Global{}).SetupWithManager(manager); err != nil {
|
||||
if err = (&resources.Global{}).SetupWithManager(manager, controllerConfig); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "resources.Global")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&resources.Namespaced{}).SetupWithManager(manager); err != nil {
|
||||
if err = (&resources.Namespaced{}).SetupWithManager(manager, controllerConfig); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "resources.Namespaced")
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -316,6 +324,7 @@ func main() {
|
||||
ctrl.Log.WithName("controllers").WithName("ResourcePools"),
|
||||
manager,
|
||||
manager.GetEventRecorderFor("pools-ctrl"),
|
||||
controllerConfig,
|
||||
); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "resourcepools")
|
||||
os.Exit(1)
|
||||
|
||||
@@ -40,7 +40,7 @@ func (c *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
panic(errors.Wrap(err, "Invalid configuration for protected Namespace regex"))
|
||||
}
|
||||
|
||||
c.Log.Info("CapsuleConfiguration reconciliation finished", "request.name", request.Name)
|
||||
c.Log.V(5).Info("CapsuleConfiguration reconciliation finished", "request.name", request.Name)
|
||||
|
||||
return
|
||||
return res, err
|
||||
}
|
||||
|
||||
@@ -12,11 +12,13 @@ import (
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
log2 "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/controllers/utils"
|
||||
capsuleutils "github.com/projectcapsule/capsule/pkg/utils"
|
||||
webhookutils "github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
)
|
||||
@@ -26,13 +28,42 @@ type Controller struct {
|
||||
label string
|
||||
}
|
||||
|
||||
func (c *Controller) SetupWithManager(mgr ctrl.Manager, cfg utils.ControllerOptions) error {
|
||||
label, err := capsuleutils.GetTypeLabel(&capsulev1beta2.Tenant{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.client = mgr.GetClient()
|
||||
c.label = label
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&corev1.PersistentVolume{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
|
||||
pv, ok := object.(*corev1.PersistentVolume)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if pv.Spec.ClaimRef == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
labels := object.GetLabels()
|
||||
_, ok = labels[c.label]
|
||||
|
||||
return !ok
|
||||
}))).
|
||||
WithOptions(controller.Options{MaxConcurrentReconciles: cfg.MaxConcurrentReconciles}).
|
||||
Complete(c)
|
||||
}
|
||||
|
||||
func (c *Controller) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
|
||||
log := log2.FromContext(ctx)
|
||||
|
||||
persistentVolume := corev1.PersistentVolume{}
|
||||
if err := c.client.Get(ctx, request.NamespacedName, &persistentVolume); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
log.Info("skipping reconciliation, resource may have been deleted")
|
||||
log.V(3).Info("skipping reconciliation, resource may have been deleted")
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
@@ -56,7 +87,7 @@ func (c *Controller) Reconcile(ctx context.Context, request reconcile.Request) (
|
||||
}
|
||||
|
||||
if tnt == nil {
|
||||
log.Info("skipping reconciliation, PV is claimed by a PVC not managed in a Tenant")
|
||||
log.V(4).Info("skipping reconciliation, PV is claimed by a PVC not managed in a Tenant")
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
@@ -87,31 +118,3 @@ func (c *Controller) Reconcile(ctx context.Context, request reconcile.Request) (
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
func (c *Controller) SetupWithManager(mgr ctrl.Manager) error {
|
||||
label, err := capsuleutils.GetTypeLabel(&capsulev1beta2.Tenant{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.client = mgr.GetClient()
|
||||
c.label = label
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&corev1.PersistentVolume{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
|
||||
pv, ok := object.(*corev1.PersistentVolume)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if pv.Spec.ClaimRef == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
labels := object.GetLabels()
|
||||
_, ok = labels[c.label]
|
||||
|
||||
return !ok
|
||||
}))).
|
||||
Complete(c)
|
||||
}
|
||||
|
||||
@@ -9,9 +9,11 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -23,6 +25,7 @@ import (
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/controllers/utils"
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
@@ -47,17 +50,31 @@ func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, config
|
||||
Watches(&capsulev1beta2.CapsuleConfiguration{}, handler.Funcs{
|
||||
UpdateFunc: func(ctx context.Context, updateEvent event.TypedUpdateEvent[client.Object], limitingInterface workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
if updateEvent.ObjectNew.GetName() == configurationName {
|
||||
if crbErr := r.EnsureClusterRoleBindings(ctx); crbErr != nil {
|
||||
if crbErr := r.EnsureClusterRoleBindingsProvisioner(ctx); crbErr != nil {
|
||||
r.Log.Error(err, "cannot update ClusterRoleBinding upon CapsuleConfiguration update")
|
||||
}
|
||||
}
|
||||
},
|
||||
}).Complete(r)
|
||||
}).
|
||||
Watches(&corev1.ServiceAccount{}, handler.Funcs{
|
||||
CreateFunc: func(ctx context.Context, e event.TypedCreateEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
r.handleSAChange(ctx, e.Object)
|
||||
},
|
||||
UpdateFunc: func(ctx context.Context, e event.TypedUpdateEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
if promotionLabelsChanged(e.ObjectOld.GetLabels(), e.ObjectNew.GetLabels()) {
|
||||
r.handleSAChange(ctx, e.ObjectNew)
|
||||
}
|
||||
},
|
||||
DeleteFunc: func(ctx context.Context, e event.TypedDeleteEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
r.handleSAChange(ctx, e.Object)
|
||||
},
|
||||
}).
|
||||
Complete(r)
|
||||
if crbErr != nil {
|
||||
err = errors.Join(err, crbErr)
|
||||
}
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// Reconcile serves both required ClusterRole and ClusterRoleBinding resources: that's ok, we're watching for multiple
|
||||
@@ -71,8 +88,8 @@ func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
break
|
||||
}
|
||||
|
||||
if err = r.EnsureClusterRoleBindings(ctx); err != nil {
|
||||
r.Log.Error(err, "Reconciliation for ClusterRoleBindings failed")
|
||||
if err = r.EnsureClusterRoleBindingsProvisioner(ctx); err != nil {
|
||||
r.Log.Error(err, "Reconciliation for ClusterRoleBindings (Provisioner) failed")
|
||||
|
||||
break
|
||||
}
|
||||
@@ -82,39 +99,55 @@ func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (r *Manager) EnsureClusterRoleBindings(ctx context.Context) (err error) {
|
||||
func (r *Manager) EnsureClusterRoleBindingsProvisioner(ctx context.Context) error {
|
||||
crb := &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: ProvisionerRoleName,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: ProvisionerRoleName},
|
||||
}
|
||||
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, crb, func() (err error) {
|
||||
crb.RoleRef = provisionerClusterRoleBinding.RoleRef
|
||||
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, crb, func() error {
|
||||
crb.RoleRef = provisionerClusterRoleBinding.RoleRef
|
||||
crb.Subjects = nil
|
||||
|
||||
crb.Subjects = []rbacv1.Subject{}
|
||||
for _, group := range r.Configuration.UserGroups() {
|
||||
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
|
||||
Kind: rbacv1.GroupKind,
|
||||
Name: group,
|
||||
})
|
||||
}
|
||||
|
||||
for _, group := range r.Configuration.UserGroups() {
|
||||
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
|
||||
Kind: "Group",
|
||||
Name: group,
|
||||
})
|
||||
}
|
||||
for _, user := range r.Configuration.UserNames() {
|
||||
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
|
||||
Kind: rbacv1.UserKind,
|
||||
Name: user,
|
||||
})
|
||||
}
|
||||
|
||||
for _, user := range r.Configuration.UserNames() {
|
||||
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
|
||||
Kind: "User",
|
||||
Name: user,
|
||||
})
|
||||
}
|
||||
if r.Configuration.AllowServiceAccountPromotion() {
|
||||
saList := &corev1.ServiceAccountList{}
|
||||
if err := r.Client.List(ctx, saList, client.MatchingLabels{
|
||||
meta.OwnerPromotionLabel: meta.OwnerPromotionLabelTrigger,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
for _, sa := range saList.Items {
|
||||
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
|
||||
Kind: rbacv1.ServiceAccountKind,
|
||||
Name: sa.Name,
|
||||
Namespace: sa.Namespace,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Manager) EnsureClusterRole(ctx context.Context, roleName string) (err error) {
|
||||
@@ -135,7 +168,7 @@ func (r *Manager) EnsureClusterRole(ctx context.Context, roleName string) (err e
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// Start is the Runnable function triggered upon Manager start-up to perform the first RBAC reconciliation
|
||||
@@ -143,7 +176,7 @@ func (r *Manager) EnsureClusterRole(ctx context.Context, roleName string) (err e
|
||||
// is handled by the Reconciler implemented interface.
|
||||
func (r *Manager) Start(ctx context.Context) error {
|
||||
for roleName := range clusterRoles {
|
||||
r.Log.Info("setting up ClusterRoles", "ClusterRole", roleName)
|
||||
r.Log.V(4).Info("setting up ClusterRoles", "ClusterRole", roleName)
|
||||
|
||||
if err := r.EnsureClusterRole(ctx, roleName); err != nil {
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
@@ -154,9 +187,9 @@ func (r *Manager) Start(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
r.Log.Info("setting up ClusterRoleBindings")
|
||||
r.Log.V(4).Info("setting up ClusterRoleBindings")
|
||||
|
||||
if err := r.EnsureClusterRoleBindings(ctx); err != nil {
|
||||
if err := r.EnsureClusterRoleBindingsProvisioner(ctx); err != nil {
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
return nil
|
||||
}
|
||||
@@ -166,3 +199,30 @@ func (r *Manager) Start(ctx context.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Manager) handleSAChange(ctx context.Context, obj client.Object) {
|
||||
if !r.Configuration.AllowServiceAccountPromotion() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.EnsureClusterRoleBindingsProvisioner(ctx); err != nil {
|
||||
r.Log.Error(err, "cannot update ClusterRoleBinding upon ServiceAccount event")
|
||||
}
|
||||
}
|
||||
|
||||
func promotionLabelsChanged(oldLabels, newLabels map[string]string) bool {
|
||||
keys := []string{
|
||||
meta.OwnerPromotionLabel,
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
oldVal, oldOK := oldLabels[key]
|
||||
newVal, newOK := newLabels[key]
|
||||
|
||||
if oldOK != newOK || oldVal != newVal {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@ import (
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/controllers/utils"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/metrics"
|
||||
@@ -35,7 +37,7 @@ type resourceClaimController struct {
|
||||
recorder record.EventRecorder
|
||||
}
|
||||
|
||||
func (r *resourceClaimController) SetupWithManager(mgr ctrl.Manager) error {
|
||||
func (r *resourceClaimController) SetupWithManager(mgr ctrl.Manager, cfg utils.ControllerOptions) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&capsulev1beta2.ResourcePoolClaim{}).
|
||||
Watches(
|
||||
@@ -43,6 +45,7 @@ func (r *resourceClaimController) SetupWithManager(mgr ctrl.Manager) error {
|
||||
handler.EnqueueRequestsFromMapFunc(r.claimsWithoutPoolFromNamespaces),
|
||||
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
|
||||
).
|
||||
WithOptions(controller.Options{MaxConcurrentReconciles: cfg.MaxConcurrentReconciles}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
@@ -52,7 +55,7 @@ func (r resourceClaimController) Reconcile(ctx context.Context, request ctrl.Req
|
||||
instance := &capsulev1beta2.ResourcePoolClaim{}
|
||||
if err = r.Get(ctx, request.NamespacedName, instance); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
log.V(5).Info("Request object not found, could have been deleted after reconcile request")
|
||||
log.V(3).Info("Request object not found, could have been deleted after reconcile request")
|
||||
|
||||
r.metrics.DeleteClaimMetric(request.Name, request.Namespace)
|
||||
|
||||
@@ -61,7 +64,7 @@ func (r resourceClaimController) Reconcile(ctx context.Context, request ctrl.Req
|
||||
|
||||
log.Error(err, "Error reading the object")
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Ensuring the Quota Status
|
||||
@@ -207,7 +210,7 @@ func (r resourceClaimController) allocateResourcePool(
|
||||
}
|
||||
|
||||
if !meta.HasLooseOwnerReference(cl, pool) {
|
||||
log.V(5).Info("adding ownerreference for", "pool", pool.Name)
|
||||
log.V(4).Info("adding ownerreference for", "pool", pool.Name)
|
||||
|
||||
patch := client.MergeFrom(cl.DeepCopy())
|
||||
|
||||
@@ -291,5 +294,5 @@ func updateStatusAndEmitEvent(
|
||||
claim.Status.Condition.Message,
|
||||
)
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
|
||||
"github.com/projectcapsule/capsule/controllers/utils"
|
||||
"github.com/projectcapsule/capsule/pkg/metrics"
|
||||
)
|
||||
|
||||
@@ -17,13 +18,14 @@ func Add(
|
||||
log logr.Logger,
|
||||
mgr manager.Manager,
|
||||
recorder record.EventRecorder,
|
||||
cfg utils.ControllerOptions,
|
||||
) (err error) {
|
||||
if err = (&resourcePoolController{
|
||||
Client: mgr.GetClient(),
|
||||
log: log.WithName("Pools"),
|
||||
recorder: recorder,
|
||||
metrics: metrics.MustMakeResourcePoolRecorder(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
}).SetupWithManager(mgr, cfg); err != nil {
|
||||
return fmt.Errorf("unable to create pool controller: %w", err)
|
||||
}
|
||||
|
||||
@@ -32,7 +34,7 @@ func Add(
|
||||
log: log.WithName("Claims"),
|
||||
recorder: recorder,
|
||||
metrics: metrics.MustMakeClaimRecorder(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
}).SetupWithManager(mgr, cfg); err != nil {
|
||||
return fmt.Errorf("unable to create claim controller: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,13 @@ import (
|
||||
"k8s.io/client-go/util/retry"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
ctrlutils "github.com/projectcapsule/capsule/controllers/utils"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/metrics"
|
||||
@@ -39,7 +41,7 @@ type resourcePoolController struct {
|
||||
recorder record.EventRecorder
|
||||
}
|
||||
|
||||
func (r *resourcePoolController) SetupWithManager(mgr ctrl.Manager) error {
|
||||
func (r *resourcePoolController) SetupWithManager(mgr ctrl.Manager, cfg ctrlutils.ControllerOptions) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&capsulev1beta2.ResourcePool{}).
|
||||
Owns(&corev1.ResourceQuota{}).
|
||||
@@ -67,6 +69,7 @@ func (r *resourcePoolController) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return requests
|
||||
}),
|
||||
).
|
||||
WithOptions(controller.Options{MaxConcurrentReconciles: cfg.MaxConcurrentReconciles}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
@@ -76,7 +79,7 @@ func (r resourcePoolController) Reconcile(ctx context.Context, request ctrl.Requ
|
||||
instance := &capsulev1beta2.ResourcePool{}
|
||||
if err = r.Get(ctx, request.NamespacedName, instance); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
log.V(5).Info("Request object not found, could have been deleted after reconcile request")
|
||||
log.V(3).Info("Request object not found, could have been deleted after reconcile request")
|
||||
|
||||
r.metrics.DeleteResourcePoolMetric(request.Name)
|
||||
|
||||
@@ -85,7 +88,7 @@ func (r resourcePoolController) Reconcile(ctx context.Context, request ctrl.Requ
|
||||
|
||||
log.Error(err, "Error reading the object")
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ResourceQuota Reconciliation
|
||||
@@ -104,9 +107,11 @@ func (r resourcePoolController) Reconcile(ctx context.Context, request ctrl.Requ
|
||||
|
||||
return r.Client.Status().Update(ctx, current)
|
||||
})
|
||||
if reconcileErr != nil || err != nil {
|
||||
log.V(3).Info("Failed to reconcile ResourcePool", "error", err)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if reconcileErr != nil {
|
||||
return ctrl.Result{}, reconcileErr
|
||||
}
|
||||
|
||||
@@ -298,7 +303,7 @@ func (r *resourcePoolController) canClaimWithinNamespace(
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return res
|
||||
}
|
||||
|
||||
// Handles exhaustions when a exhaustion was already declared in the given map.
|
||||
@@ -336,7 +341,7 @@ func (r *resourcePoolController) handleClaimOrderedExhaustion(
|
||||
return queued, updateStatusAndEmitEvent(ctx, r.Client, r.recorder, claim, cond)
|
||||
}
|
||||
|
||||
return
|
||||
return queued, err
|
||||
}
|
||||
|
||||
func (r *resourcePoolController) handleClaimResourceExhaustion(
|
||||
@@ -399,12 +404,12 @@ func (r *resourcePoolController) handleClaimToPoolBinding(
|
||||
cond.Message = "Claimed resources"
|
||||
|
||||
if err = updateStatusAndEmitEvent(ctx, r.Client, r.recorder, claim, cond); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
pool.AddClaimToStatus(claim)
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// Attempts to garbage collect a ResourceQuota resource.
|
||||
@@ -571,7 +576,7 @@ func (r *resourcePoolController) gatherMatchingNamespaces(
|
||||
seenNamespaces := make(map[string]struct{})
|
||||
|
||||
if !pool.DeletionTimestamp.IsZero() {
|
||||
return
|
||||
return namespaces, err
|
||||
}
|
||||
|
||||
for _, selector := range pool.Spec.Selectors {
|
||||
@@ -597,7 +602,7 @@ func (r *resourcePoolController) gatherMatchingNamespaces(
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return namespaces, err
|
||||
}
|
||||
|
||||
// Get Currently selected claims for the resourcepool.
|
||||
|
||||
@@ -16,12 +16,14 @@ import (
|
||||
"sigs.k8s.io/cluster-api/util/patch"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
ctrllog "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/controllers/utils"
|
||||
)
|
||||
|
||||
type Global struct {
|
||||
@@ -29,7 +31,7 @@ type Global struct {
|
||||
processor Processor
|
||||
}
|
||||
|
||||
func (r *Global) SetupWithManager(mgr ctrl.Manager) error {
|
||||
func (r *Global) SetupWithManager(mgr ctrl.Manager, cfg utils.ControllerOptions) error {
|
||||
r.client = mgr.GetClient()
|
||||
r.processor = Processor{
|
||||
client: mgr.GetClient(),
|
||||
@@ -38,6 +40,7 @@ func (r *Global) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&capsulev1beta2.GlobalTenantResource{}).
|
||||
Watches(&capsulev1beta2.Tenant{}, handler.EnqueueRequestsFromMapFunc(r.enqueueRequestFromTenant)).
|
||||
WithOptions(controller.Options{MaxConcurrentReconciles: cfg.MaxConcurrentReconciles}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
@@ -46,12 +49,12 @@ func (r *Global) Reconcile(ctx context.Context, request reconcile.Request) (reco
|
||||
|
||||
log := ctrllog.FromContext(ctx)
|
||||
|
||||
log.Info("start processing")
|
||||
log.V(4).Info("start processing")
|
||||
// Retrieving the GlobalTenantResource
|
||||
tntResource := &capsulev1beta2.GlobalTenantResource{}
|
||||
if err = r.client.Get(ctx, request.NamespacedName, tntResource); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
log.Info("Request object not found, could have been deleted after reconcile request")
|
||||
log.V(3).Info("Request object not found, could have been deleted after reconcile request")
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
@@ -188,7 +191,7 @@ func (r *Global) reconcileNormal(ctx context.Context, tntResource *capsulev1beta
|
||||
|
||||
tntResource.Status.SelectedTenants = tntSet.List()
|
||||
|
||||
log.Info("processing completed")
|
||||
log.V(4).Info("processing completed")
|
||||
|
||||
return reconcile.Result{Requeue: true, RequeueAfter: tntResource.Spec.ResyncPeriod.Duration}, nil
|
||||
}
|
||||
@@ -202,7 +205,7 @@ func (r *Global) reconcileDelete(ctx context.Context, tntResource *capsulev1beta
|
||||
controllerutil.RemoveFinalizer(tntResource, finalizer)
|
||||
}
|
||||
|
||||
log.Info("processing completed")
|
||||
log.V(4).Info("processing completed")
|
||||
|
||||
return reconcile.Result{Requeue: true, RequeueAfter: tntResource.Spec.ResyncPeriod.Duration}, nil
|
||||
}
|
||||
|
||||
@@ -14,11 +14,13 @@ import (
|
||||
"sigs.k8s.io/cluster-api/util/patch"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
ctrllog "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/controllers/utils"
|
||||
)
|
||||
|
||||
type Namespaced struct {
|
||||
@@ -26,7 +28,7 @@ type Namespaced struct {
|
||||
processor Processor
|
||||
}
|
||||
|
||||
func (r *Namespaced) SetupWithManager(mgr ctrl.Manager) error {
|
||||
func (r *Namespaced) SetupWithManager(mgr ctrl.Manager, cfg utils.ControllerOptions) error {
|
||||
r.client = mgr.GetClient()
|
||||
r.processor = Processor{
|
||||
client: mgr.GetClient(),
|
||||
@@ -34,18 +36,19 @@ func (r *Namespaced) SetupWithManager(mgr ctrl.Manager) error {
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&capsulev1beta2.TenantResource{}).
|
||||
WithOptions(controller.Options{MaxConcurrentReconciles: cfg.MaxConcurrentReconciles}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *Namespaced) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
|
||||
log := ctrllog.FromContext(ctx)
|
||||
|
||||
log.Info("start processing")
|
||||
log.V(4).Info("start processing")
|
||||
// Retrieving the TenantResource
|
||||
tntResource := &capsulev1beta2.TenantResource{}
|
||||
if err := r.client.Get(ctx, request.NamespacedName, tntResource); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
log.Info("Request object not found, could have been deleted after reconcile request")
|
||||
log.V(3).Info("Request object not found, could have been deleted after reconcile request")
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
@@ -97,7 +100,7 @@ func (r *Namespaced) reconcileNormal(ctx context.Context, tntResource *capsulev1
|
||||
}
|
||||
|
||||
if len(tl.Items) == 0 {
|
||||
log.Info("skipping sync, the current Namespace is not belonging to any Global")
|
||||
log.V(4).Info("skipping sync, the current Namespace is not belonging to any Global")
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
@@ -143,7 +146,7 @@ func (r *Namespaced) reconcileNormal(ctx context.Context, tntResource *capsulev1
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("processing completed")
|
||||
log.V(4).Info("processing completed")
|
||||
|
||||
return reconcile.Result{Requeue: true, RequeueAfter: tntResource.Spec.ResyncPeriod.Duration}, nil
|
||||
}
|
||||
@@ -157,7 +160,7 @@ func (r *Namespaced) reconcileDelete(ctx context.Context, tntResource *capsulev1
|
||||
|
||||
controllerutil.RemoveFinalizer(tntResource, finalizer)
|
||||
|
||||
log.Info("processing completed")
|
||||
log.V(4).Info("processing completed")
|
||||
|
||||
return reconcile.Result{Requeue: true, RequeueAfter: tntResource.Spec.ResyncPeriod.Duration}, nil
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ func (r *Manager) syncLimitRange(ctx context.Context, tenant *capsulev1beta2.Ten
|
||||
|
||||
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring LimitRange %s", target.GetName()), err)
|
||||
|
||||
r.Log.Info("LimitRange sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
|
||||
r.Log.V(4).Info("LimitRange sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -5,21 +5,27 @@ package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/client-go/util/retry"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/controllers/utils"
|
||||
meta "github.com/projectcapsule/capsule/pkg/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/metrics"
|
||||
)
|
||||
|
||||
@@ -32,7 +38,7 @@ type Manager struct {
|
||||
RESTConfig *rest.Config
|
||||
}
|
||||
|
||||
func (r *Manager) SetupWithManager(mgr ctrl.Manager) error {
|
||||
func (r *Manager) SetupWithManager(mgr ctrl.Manager, cfg utils.ControllerOptions) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&capsulev1beta2.Tenant{}).
|
||||
Owns(&networkingv1.NetworkPolicy{}).
|
||||
@@ -40,126 +46,141 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager) error {
|
||||
Owns(&corev1.ResourceQuota{}).
|
||||
Owns(&rbacv1.RoleBinding{}).
|
||||
Watches(&corev1.Namespace{}, handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &capsulev1beta2.Tenant{})).
|
||||
WithOptions(controller.Options{MaxConcurrentReconciles: cfg.MaxConcurrentReconciles}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
//nolint:nakedret
|
||||
func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ctrl.Result, err error) {
|
||||
r.Log = r.Log.WithValues("Request.Name", request.Name)
|
||||
// Fetch the Tenant instance
|
||||
instance := &capsulev1beta2.Tenant{}
|
||||
if err = r.Get(ctx, request.NamespacedName, instance); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
r.Log.Info("Request object not found, could have been deleted after reconcile request")
|
||||
r.Log.V(3).Info("Request object not found, could have been deleted after reconcile request")
|
||||
|
||||
// If tenant was deleted or cannot be found, clean up metrics
|
||||
r.Metrics.DeleteAllMetrics(request.Name)
|
||||
r.Metrics.DeleteAllMetricsForTenant(request.Name)
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
r.Log.Error(err, "Error reading the object")
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
|
||||
preRecNamespaces := instance.Status.Namespaces
|
||||
defer func() {
|
||||
r.syncTenantStatusMetrics(instance)
|
||||
|
||||
// Ensuring the Tenant Status
|
||||
if err = r.updateTenantStatus(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot update Tenant status")
|
||||
if uerr := r.updateTenantStatus(ctx, instance, err); uerr != nil {
|
||||
err = fmt.Errorf("cannot update tenant status: %w", uerr)
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// Ensuring Metadata.
|
||||
err, updated := r.ensureMetadata(ctx, instance)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("cannot ensure metadata: %w", err)
|
||||
|
||||
return result, err
|
||||
}
|
||||
// Ensuring Metadata
|
||||
if err = r.ensureMetadata(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot ensure metadata")
|
||||
|
||||
return
|
||||
if updated {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Ensuring ResourceQuota
|
||||
r.Log.Info("Ensuring limit resources count is updated")
|
||||
r.Log.V(4).Info("Ensuring limit resources count is updated")
|
||||
|
||||
if err = r.syncCustomResourceQuotaUsages(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot count limited resources")
|
||||
err = fmt.Errorf("cannot count limited resources: %w", err)
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
// Ensuring all namespaces are collected
|
||||
r.Log.Info("Ensuring all Namespaces are collected")
|
||||
|
||||
if err = r.collectNamespaces(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot collect Namespace resources")
|
||||
// Reconcile Namespaces
|
||||
r.Log.V(4).Info("Starting processing of Namespaces", "items", len(instance.Status.Namespaces))
|
||||
|
||||
return
|
||||
if err = r.reconcileNamespaces(ctx, instance); err != nil {
|
||||
err = fmt.Errorf("namespace(s) had reconciliation errors")
|
||||
|
||||
return result, err
|
||||
}
|
||||
// Ensuring Status metrics are exposed
|
||||
r.Log.Info("Ensuring all status metrics are exposed")
|
||||
r.syncStatusMetrics(instance, preRecNamespaces)
|
||||
|
||||
// Ensuring Namespace metadata
|
||||
r.Log.Info("Starting processing of Namespaces", "items", len(instance.Status.Namespaces))
|
||||
|
||||
if err = r.syncNamespaces(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync Namespace items")
|
||||
|
||||
return
|
||||
}
|
||||
// Ensuring NetworkPolicy resources
|
||||
r.Log.Info("Starting processing of Network Policies")
|
||||
r.Log.V(4).Info("Starting processing of Network Policies")
|
||||
|
||||
if err = r.syncNetworkPolicies(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync NetworkPolicy items")
|
||||
err = fmt.Errorf("cannot sync networkPolicy items: %w", err)
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
// Ensuring LimitRange resources
|
||||
r.Log.Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges.Items))
|
||||
r.Log.V(4).Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges.Items))
|
||||
|
||||
if err = r.syncLimitRanges(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync LimitRange items")
|
||||
err = fmt.Errorf("cannot sync limitrange items: %w", err)
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
// Ensuring ResourceQuota resources
|
||||
r.Log.Info("Starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota.Items))
|
||||
r.Log.V(4).Info("Starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota.Items))
|
||||
|
||||
if err = r.syncResourceQuotas(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync ResourceQuota items")
|
||||
err = fmt.Errorf("cannot sync resourcequota items: %w", err)
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
// Ensuring RoleBinding resources
|
||||
r.Log.Info("Ensuring RoleBindings for Owners and Tenant")
|
||||
r.Log.V(4).Info("Ensuring RoleBindings for Owners and Tenant")
|
||||
|
||||
if err = r.syncRoleBindings(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync RoleBindings items")
|
||||
err = fmt.Errorf("cannot sync rolebindings items: %w", err)
|
||||
|
||||
return
|
||||
}
|
||||
// Ensuring Namespace count
|
||||
r.Log.Info("Ensuring Namespace count")
|
||||
|
||||
if err = r.ensureNamespaceCount(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync Namespace count")
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
|
||||
r.Log.Info("Tenant reconciling completed")
|
||||
r.Log.V(4).Info("Tenant reconciling completed")
|
||||
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
func (r *Manager) updateTenantStatus(ctx context.Context, tnt *capsulev1beta2.Tenant) error {
|
||||
func (r *Manager) updateTenantStatus(ctx context.Context, tnt *capsulev1beta2.Tenant, reconcileError error) error {
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
||||
if tnt.Spec.Cordoned {
|
||||
tnt.Status.State = capsulev1beta2.TenantStateCordoned
|
||||
} else {
|
||||
tnt.Status.State = capsulev1beta2.TenantStateActive
|
||||
latest := &capsulev1beta2.Tenant{}
|
||||
if err = r.Get(ctx, types.NamespacedName{Name: tnt.GetName()}, latest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.Client.Status().Update(ctx, tnt)
|
||||
latest.Status = tnt.Status
|
||||
|
||||
// Set Ready Condition
|
||||
readyCondition := meta.NewReadyCondition(tnt)
|
||||
if reconcileError != nil {
|
||||
readyCondition.Message = reconcileError.Error()
|
||||
readyCondition.Status = metav1.ConditionFalse
|
||||
readyCondition.Reason = meta.FailedReason
|
||||
}
|
||||
|
||||
latest.Status.Conditions.UpdateConditionByType(readyCondition)
|
||||
|
||||
// Set Cordoned Condition
|
||||
cordonedCondition := meta.NewCordonedCondition(tnt)
|
||||
|
||||
if tnt.Spec.Cordoned {
|
||||
latest.Status.State = capsulev1beta2.TenantStateCordoned
|
||||
|
||||
cordonedCondition.Reason = meta.CordonedReason
|
||||
cordonedCondition.Message = "Tenant is cordoned"
|
||||
cordonedCondition.Status = metav1.ConditionTrue
|
||||
} else {
|
||||
latest.Status.State = capsulev1beta2.TenantStateActive
|
||||
}
|
||||
|
||||
latest.Status.Conditions.UpdateConditionByType(cordonedCondition)
|
||||
|
||||
return r.Client.Status().Update(ctx, latest)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,13 +11,19 @@ import (
|
||||
)
|
||||
|
||||
// Sets a label on the Tenant object with it's name.
|
||||
func (r *Manager) ensureMetadata(ctx context.Context, tnt *capsulev1beta2.Tenant) (err error) {
|
||||
func (r *Manager) ensureMetadata(ctx context.Context, tnt *capsulev1beta2.Tenant) (err error, changed bool) {
|
||||
// Assign Labels
|
||||
if tnt.Labels == nil {
|
||||
tnt.Labels = make(map[string]string)
|
||||
}
|
||||
|
||||
tnt.Labels[capsuleapi.TenantNameLabel] = tnt.Name
|
||||
if v, ok := tnt.Labels[capsuleapi.TenantNameLabel]; !ok || v != tnt.Name {
|
||||
if err := r.Update(ctx, tnt); err != nil {
|
||||
return err, false
|
||||
}
|
||||
|
||||
return r.Update(ctx, tnt)
|
||||
return nil, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -3,32 +3,58 @@
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"slices"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
)
|
||||
|
||||
// Exposing Status Metrics for tenant.
|
||||
func (r *Manager) syncStatusMetrics(tenant *capsulev1beta2.Tenant, preRecNamespaces []string) {
|
||||
var cordoned float64 = 0
|
||||
|
||||
func (r *Manager) syncTenantStatusMetrics(tenant *capsulev1beta2.Tenant) {
|
||||
// Expose namespace-tenant relationship
|
||||
for _, ns := range tenant.Status.Namespaces {
|
||||
r.Metrics.TenantNamespaceRelationshipGauge.WithLabelValues(tenant.GetName(), ns).Set(1)
|
||||
}
|
||||
|
||||
// Cleanup deleted namespaces
|
||||
for _, ns := range preRecNamespaces {
|
||||
if !slices.Contains(tenant.Status.Namespaces, ns) {
|
||||
r.Metrics.DeleteNamespaceRelationshipMetrics(ns)
|
||||
}
|
||||
}
|
||||
|
||||
if tenant.Spec.Cordoned {
|
||||
cordoned = 1
|
||||
}
|
||||
// Expose cordoned status
|
||||
r.Metrics.TenantNamespaceCounterGauge.WithLabelValues(tenant.Name).Set(float64(tenant.Status.Size))
|
||||
// Expose the namespace counter
|
||||
r.Metrics.TenantCordonedStatusGauge.WithLabelValues(tenant.Name).Set(cordoned)
|
||||
|
||||
// Expose Status Metrics
|
||||
for _, status := range []string{meta.ReadyCondition, meta.CordonedCondition} {
|
||||
var value float64
|
||||
|
||||
cond := tenant.Status.Conditions.GetConditionByType(status)
|
||||
if cond == nil {
|
||||
r.Metrics.DeleteTenantConditionMetricByType(tenant.Name, status)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if cond.Status == metav1.ConditionTrue {
|
||||
value = 1
|
||||
}
|
||||
|
||||
r.Metrics.TenantConditionGauge.WithLabelValues(tenant.GetName(), status).Set(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Exposing Status Metrics for tenant.
|
||||
func (r *Manager) syncNamespaceStatusMetrics(tenant *capsulev1beta2.Tenant, namespace *corev1.Namespace) {
|
||||
for _, status := range []string{meta.ReadyCondition, meta.CordonedCondition} {
|
||||
var value float64
|
||||
|
||||
cond := tenant.Status.Conditions.GetConditionByType(status)
|
||||
if cond == nil {
|
||||
r.Metrics.DeleteTenantNamespaceConditionMetricByType(namespace.Name, status)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if cond.Status == metav1.ConditionTrue {
|
||||
value = 1
|
||||
}
|
||||
|
||||
r.Metrics.TenantNamespaceConditionGauge.WithLabelValues(tenant.GetName(), namespace.GetName(), status).Set(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/valyala/fasttemplate"
|
||||
"golang.org/x/sync/errgroup"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
@@ -20,51 +21,212 @@ import (
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/utils"
|
||||
)
|
||||
|
||||
// Ensuring all annotations are applied to each Namespace handled by the Tenant.
|
||||
func (r *Manager) syncNamespaces(ctx context.Context, tenant *capsulev1beta2.Tenant) (err error) {
|
||||
func (r *Manager) reconcileNamespaces(ctx context.Context, tenant *capsulev1beta2.Tenant) (err error) {
|
||||
if err = r.collectNamespaces(ctx, tenant); err != nil {
|
||||
err = fmt.Errorf("cannot collect namespaces: %w", err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
gcSet := make(map[string]struct{})
|
||||
for _, inst := range tenant.Status.Spaces {
|
||||
gcSet[inst.Name] = struct{}{}
|
||||
}
|
||||
|
||||
group := new(errgroup.Group)
|
||||
|
||||
for _, item := range tenant.Status.Namespaces {
|
||||
namespace := item
|
||||
|
||||
delete(gcSet, namespace)
|
||||
|
||||
group.Go(func() error {
|
||||
return r.syncNamespaceMetadata(ctx, namespace, tenant)
|
||||
return r.reconcileNamespace(ctx, namespace, tenant)
|
||||
})
|
||||
}
|
||||
|
||||
if err = group.Wait(); err != nil {
|
||||
r.Log.Error(err, "Cannot sync Namespaces")
|
||||
|
||||
err = fmt.Errorf("cannot sync Namespaces: %w", err)
|
||||
}
|
||||
|
||||
return
|
||||
for name := range gcSet {
|
||||
r.Metrics.DeleteAllMetricsForNamespace(name)
|
||||
|
||||
tenant.Status.RemoveInstance(&capsulev1beta2.TenantStatusNamespaceItem{
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
|
||||
tenant.Status.Size = uint(len(tenant.Status.Namespaces))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Manager) syncNamespaceMetadata(ctx context.Context, namespace string, tnt *capsulev1beta2.Tenant) (err error) {
|
||||
var res controllerutil.OperationResult
|
||||
func (r *Manager) reconcileNamespace(ctx context.Context, namespace string, tnt *capsulev1beta2.Tenant) (err error) {
|
||||
ns := &corev1.Namespace{}
|
||||
if err = r.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = retry.RetryOnConflict(retry.DefaultBackoff, func() (conflictErr error) {
|
||||
ns := &corev1.Namespace{}
|
||||
if conflictErr = r.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil {
|
||||
return conflictErr
|
||||
stat := &capsulev1beta2.TenantStatusNamespaceItem{
|
||||
Name: namespace,
|
||||
UID: ns.GetUID(),
|
||||
}
|
||||
|
||||
metaStatus := &capsulev1beta2.TenantStatusNamespaceMetadata{}
|
||||
|
||||
// Always update tenant status condition after reconciliation
|
||||
defer func() {
|
||||
instance := tnt.Status.GetInstance(stat)
|
||||
if instance != nil {
|
||||
stat = instance
|
||||
}
|
||||
|
||||
res, conflictErr = controllerutil.CreateOrUpdate(ctx, r.Client, ns, func() error {
|
||||
return SyncNamespaceMetadata(tnt, ns)
|
||||
readCondition := meta.NewReadyCondition(ns)
|
||||
|
||||
if err != nil {
|
||||
readCondition.Status = metav1.ConditionFalse
|
||||
readCondition.Reason = meta.FailedReason
|
||||
readCondition.Message = fmt.Sprintf("Failed to reconcile: %v", err)
|
||||
|
||||
if instance != nil && instance.Metadata != nil {
|
||||
stat.Metadata = instance.Metadata
|
||||
}
|
||||
} else if metaStatus != nil {
|
||||
stat.Metadata = metaStatus
|
||||
}
|
||||
|
||||
stat.Conditions.UpdateConditionByType(readCondition)
|
||||
|
||||
cordonedCondition := meta.NewCordonedCondition(ns)
|
||||
|
||||
if ns.Labels[meta.CordonedLabel] == meta.CordonedLabelTrigger {
|
||||
cordonedCondition.Reason = meta.CordonedReason
|
||||
cordonedCondition.Message = "namespace is cordoned"
|
||||
cordonedCondition.Status = metav1.ConditionTrue
|
||||
}
|
||||
|
||||
stat.Conditions.UpdateConditionByType(cordonedCondition)
|
||||
|
||||
tnt.Status.UpdateInstance(stat)
|
||||
|
||||
r.syncNamespaceStatusMetrics(tnt, ns)
|
||||
}()
|
||||
|
||||
err = retry.RetryOnConflict(retry.DefaultBackoff, func() (conflictErr error) {
|
||||
_, conflictErr = controllerutil.CreateOrUpdate(ctx, r.Client, ns, func() error {
|
||||
metaStatus, err = r.reconcileMetadata(ctx, ns, tnt, stat)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return conflictErr
|
||||
})
|
||||
|
||||
r.emitEvent(tnt, namespace, res, "Ensuring Namespace metadata", err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
//nolint:nestif
|
||||
func (r *Manager) reconcileMetadata(
|
||||
ctx context.Context,
|
||||
ns *corev1.Namespace,
|
||||
tnt *capsulev1beta2.Tenant,
|
||||
stat *capsulev1beta2.TenantStatusNamespaceItem,
|
||||
) (
|
||||
managed *capsulev1beta2.TenantStatusNamespaceMetadata,
|
||||
err error,
|
||||
) {
|
||||
capsuleLabel, _ := utils.GetTypeLabel(&capsulev1beta2.Tenant{})
|
||||
|
||||
originLabels := ns.GetLabels()
|
||||
if originLabels == nil {
|
||||
originLabels = make(map[string]string)
|
||||
}
|
||||
|
||||
originAnnotations := ns.GetAnnotations()
|
||||
if originAnnotations == nil {
|
||||
originAnnotations = make(map[string]string)
|
||||
}
|
||||
|
||||
managedAnnotations := buildNamespaceAnnotationsForTenant(tnt)
|
||||
managedLabels := buildNamespaceLabelsForTenant(tnt)
|
||||
|
||||
if opts := tnt.Spec.NamespaceOptions; opts != nil && len(opts.AdditionalMetadataList) > 0 {
|
||||
for _, md := range opts.AdditionalMetadataList {
|
||||
var ok bool
|
||||
|
||||
ok, err = utils.IsNamespaceSelectedBySelector(ns, md.NamespaceSelector)
|
||||
if err != nil {
|
||||
return managed, err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
applyTemplateMap(md.Labels, tnt, ns)
|
||||
applyTemplateMap(md.Annotations, tnt, ns)
|
||||
|
||||
utils.MapMergeNoOverrite(managedLabels, md.Labels)
|
||||
utils.MapMergeNoOverrite(managedAnnotations, md.Annotations)
|
||||
}
|
||||
}
|
||||
|
||||
managedMetadataOnly := tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.ManagedMetadataOnly
|
||||
|
||||
// Handle User-Defined Metadata, if allowed
|
||||
if !managedMetadataOnly {
|
||||
if originLabels != nil {
|
||||
maps.Copy(originLabels, managedLabels)
|
||||
}
|
||||
|
||||
if originAnnotations != nil {
|
||||
maps.Copy(originAnnotations, managedAnnotations)
|
||||
}
|
||||
|
||||
// Cleanup old Metadata
|
||||
instance := tnt.Status.GetInstance(stat)
|
||||
if instance != nil && instance.Metadata != nil {
|
||||
for label := range instance.Metadata.Labels {
|
||||
if _, ok := managedLabels[label]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
delete(originLabels, label)
|
||||
}
|
||||
|
||||
for annotation := range instance.Metadata.Annotations {
|
||||
if _, ok := managedAnnotations[annotation]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
delete(originAnnotations, annotation)
|
||||
}
|
||||
}
|
||||
|
||||
managed = &capsulev1beta2.TenantStatusNamespaceMetadata{
|
||||
Labels: managedLabels,
|
||||
Annotations: managedAnnotations,
|
||||
}
|
||||
} else {
|
||||
originLabels = managedLabels
|
||||
originAnnotations = managedAnnotations
|
||||
}
|
||||
|
||||
originLabels["kubernetes.io/metadata.name"] = ns.GetName()
|
||||
originLabels[capsuleLabel] = tnt.GetName()
|
||||
|
||||
ns.SetLabels(originLabels)
|
||||
ns.SetAnnotations(originAnnotations)
|
||||
|
||||
return managed, err
|
||||
}
|
||||
|
||||
func buildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]string {
|
||||
annotations := make(map[string]string)
|
||||
|
||||
@@ -120,6 +282,35 @@ func buildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]s
|
||||
return annotations
|
||||
}
|
||||
|
||||
func buildNamespaceLabelsForTenant(tnt *capsulev1beta2.Tenant) map[string]string {
|
||||
labels := make(map[string]string)
|
||||
|
||||
if md := tnt.Spec.NamespaceOptions; md != nil && md.AdditionalMetadata != nil {
|
||||
maps.Copy(labels, md.AdditionalMetadata.Labels)
|
||||
}
|
||||
|
||||
if tnt.Spec.Cordoned {
|
||||
labels[meta.CordonedLabel] = "true"
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
func (r *Manager) collectNamespaces(ctx context.Context, tenant *capsulev1beta2.Tenant) (err error) {
|
||||
list := &corev1.NamespaceList{}
|
||||
|
||||
err = r.List(ctx, list, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".metadata.ownerReferences[*].capsule", tenant.GetName()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tenant.AssignNamespaces(list.Items)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// applyTemplateMap applies templating to all values in the provided map in place.
|
||||
func applyTemplateMap(m map[string]string, tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) {
|
||||
for k, v := range m {
|
||||
@@ -136,99 +327,3 @@ func applyTemplateMap(m map[string]string, tnt *capsulev1beta2.Tenant, ns *corev
|
||||
m[k] = tmplString
|
||||
}
|
||||
}
|
||||
|
||||
func buildNamespaceLabelsForTenant(tnt *capsulev1beta2.Tenant) map[string]string {
|
||||
labels := make(map[string]string)
|
||||
|
||||
if md := tnt.Spec.NamespaceOptions; md != nil && md.AdditionalMetadata != nil {
|
||||
maps.Copy(labels, md.AdditionalMetadata.Labels)
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
func (r *Manager) ensureNamespaceCount(ctx context.Context, tenant *capsulev1beta2.Tenant) error {
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
tenant.Status.Size = uint(len(tenant.Status.Namespaces))
|
||||
|
||||
found := &capsulev1beta2.Tenant{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: tenant.GetName()}, found); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
found.Status.Size = tenant.Status.Size
|
||||
|
||||
return r.Client.Status().Update(ctx, found, &client.SubResourceUpdateOptions{})
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Manager) collectNamespaces(ctx context.Context, tenant *capsulev1beta2.Tenant) error {
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
||||
list := &corev1.NamespaceList{}
|
||||
|
||||
err = r.List(ctx, list, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".metadata.ownerReferences[*].capsule", tenant.GetName()),
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, tenant.DeepCopy(), func() error {
|
||||
tenant.AssignNamespaces(list.Items)
|
||||
|
||||
return r.Client.Status().Update(ctx, tenant, &client.SubResourceUpdateOptions{})
|
||||
})
|
||||
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// SyncNamespaceMetadata sync namespace metadata according to tenant spec.
|
||||
func SyncNamespaceMetadata(tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) error {
|
||||
capsuleLabel, _ := utils.GetTypeLabel(&capsulev1beta2.Tenant{})
|
||||
|
||||
annotations := buildNamespaceAnnotationsForTenant(tnt)
|
||||
labels := buildNamespaceLabelsForTenant(tnt)
|
||||
|
||||
if opts := tnt.Spec.NamespaceOptions; opts != nil && len(opts.AdditionalMetadataList) > 0 {
|
||||
for _, md := range opts.AdditionalMetadataList {
|
||||
ok, err := utils.IsNamespaceSelectedBySelector(ns, md.NamespaceSelector)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
applyTemplateMap(md.Labels, tnt, ns)
|
||||
applyTemplateMap(md.Annotations, tnt, ns)
|
||||
|
||||
maps.Copy(labels, md.Labels)
|
||||
maps.Copy(annotations, md.Annotations)
|
||||
}
|
||||
}
|
||||
|
||||
labels["kubernetes.io/metadata.name"] = ns.GetName()
|
||||
labels[capsuleLabel] = tnt.GetName()
|
||||
|
||||
if tnt.Spec.Cordoned {
|
||||
ns.Labels[utils.CordonedLabel] = "true"
|
||||
} else {
|
||||
delete(ns.Labels, utils.CordonedLabel)
|
||||
}
|
||||
|
||||
if ns.Annotations == nil {
|
||||
ns.SetAnnotations(annotations)
|
||||
} else {
|
||||
maps.Copy(ns.Annotations, annotations)
|
||||
}
|
||||
|
||||
if ns.Labels == nil {
|
||||
ns.SetLabels(labels)
|
||||
} else {
|
||||
maps.Copy(ns.Labels, labels)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func (r *Manager) syncNetworkPolicy(ctx context.Context, tenant *capsulev1beta2.
|
||||
|
||||
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring NetworkPolicy %s", target.GetName()), err)
|
||||
|
||||
r.Log.Info("Network Policy sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
|
||||
r.Log.V(4).Info("Network Policy sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -39,7 +39,6 @@ import (
|
||||
//
|
||||
// In case of Namespace-scoped Resource Budget, we're just replicating the resources across all registered Namespaces.
|
||||
|
||||
//nolint:nakedret
|
||||
func (r *Manager) syncResourceQuotas(ctx context.Context, tenant *capsulev1beta2.Tenant) (err error) { //nolint:gocognit
|
||||
// getting ResourceQuota labels for the mutateFn
|
||||
var tenantLabel, typeLabel string
|
||||
@@ -103,7 +102,7 @@ func (r *Manager) syncResourceQuotas(ctx context.Context, tenant *capsulev1beta2
|
||||
// For this case, we're going to block the Quota setting the Hard as the
|
||||
// used one.
|
||||
for name, hardQuota := range resourceQuota.Hard {
|
||||
r.Log.Info("Desired hard " + name.String() + " quota is " + hardQuota.String())
|
||||
r.Log.V(4).Info("Desired hard " + name.String() + " quota is " + hardQuota.String())
|
||||
|
||||
// Getting the whole usage across all the Tenant Namespaces
|
||||
var quantity resource.Quantity
|
||||
@@ -111,7 +110,7 @@ func (r *Manager) syncResourceQuotas(ctx context.Context, tenant *capsulev1beta2
|
||||
quantity.Add(item.Status.Used[name])
|
||||
}
|
||||
|
||||
r.Log.Info("Computed " + name.String() + " quota for the whole Tenant is " + quantity.String())
|
||||
r.Log.V(4).Info("Computed " + name.String() + " quota for the whole Tenant is " + quantity.String())
|
||||
|
||||
// Expose usage and limit metrics for the resource (name) of the ResourceQuota (index)
|
||||
r.Metrics.TenantResourceUsageGauge.WithLabelValues(
|
||||
@@ -175,16 +174,16 @@ func (r *Manager) syncResourceQuotas(ctx context.Context, tenant *capsulev1beta2
|
||||
if scopeErr = r.resourceQuotasUpdate(ctx, name, quantity, toKeep, resourceQuota.Hard[name], list.Items...); scopeErr != nil {
|
||||
r.Log.Error(scopeErr, "cannot proceed with outer ResourceQuota")
|
||||
|
||||
return
|
||||
return scopeErr
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return scopeErr
|
||||
})
|
||||
}
|
||||
// Waiting the update of all ResourceQuotas
|
||||
if err = group.Wait(); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
// getting requested ResourceQuota keys
|
||||
@@ -207,7 +206,6 @@ func (r *Manager) syncResourceQuotas(ctx context.Context, tenant *capsulev1beta2
|
||||
return group.Wait()
|
||||
}
|
||||
|
||||
//nolint:nakedret
|
||||
func (r *Manager) syncResourceQuota(ctx context.Context, tenant *capsulev1beta2.Tenant, namespace string, keys []string) (err error) {
|
||||
// getting ResourceQuota labels for the mutateFn
|
||||
var tenantLabel, typeLabel string
|
||||
@@ -261,10 +259,10 @@ func (r *Manager) syncResourceQuota(ctx context.Context, tenant *capsulev1beta2.
|
||||
|
||||
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring ResourceQuota %s", target.GetName()), err)
|
||||
|
||||
r.Log.Info("Resource Quota sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
|
||||
r.Log.V(4).Info("Resource Quota sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +293,7 @@ func (r *Manager) resourceQuotasUpdate(ctx context.Context, resourceName corev1.
|
||||
group.Go(func() (err error) {
|
||||
found := &corev1.ResourceQuota{}
|
||||
if err = r.Get(ctx, types.NamespacedName{Namespace: rq.Namespace, Name: rq.Name}, found); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() (retryErr error) {
|
||||
@@ -305,12 +303,19 @@ func (r *Manager) resourceQuotasUpdate(ctx context.Context, resourceName corev1.
|
||||
if found.Annotations == nil {
|
||||
found.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
if found.Labels == nil {
|
||||
found.Labels = make(map[string]string, len(rq.Labels))
|
||||
}
|
||||
|
||||
// Pruning the Capsule quota annotations:
|
||||
// if the ResourceQuota is updated by removing some objects,
|
||||
// we could still have left-overs which could be misleading.
|
||||
// This will not lead to a reconciliation loop since the whole code is idempotent.
|
||||
for k := range found.Annotations {
|
||||
if (strings.HasPrefix(k, capsulev1beta2.HardCapsuleQuotaAnnotation) || strings.HasPrefix(k, capsulev1beta2.UsedCapsuleQuotaAnnotation)) && !annotationsToKeep.Has(k) {
|
||||
if (strings.HasPrefix(k, capsulev1beta2.HardCapsuleQuotaAnnotation) ||
|
||||
strings.HasPrefix(k, capsulev1beta2.UsedCapsuleQuotaAnnotation)) &&
|
||||
(annotationsToKeep == nil || !annotationsToKeep.Has(k)) {
|
||||
delete(found.Annotations, k)
|
||||
}
|
||||
}
|
||||
@@ -323,8 +328,14 @@ func (r *Manager) resourceQuotasUpdate(ctx context.Context, resourceName corev1.
|
||||
if limitKey, keyErr := capsulev1beta2.HardQuotaFor(resourceName); keyErr == nil {
|
||||
found.Annotations[limitKey] = limit.String()
|
||||
}
|
||||
|
||||
// Updating the Resource according to the actual.Cmp result
|
||||
found.Spec.Hard = rq.Spec.Hard
|
||||
if rq.Spec.Hard != nil {
|
||||
found.Spec.Hard = rq.Spec.Hard.DeepCopy()
|
||||
} else {
|
||||
// Ensure it’s nil (or empty) consistently
|
||||
found.Spec.Hard = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -35,7 +35,7 @@ func (r *Manager) syncCustomResourceQuotaUsages(ctx context.Context, tenant *cap
|
||||
|
||||
parts := strings.Split(k, "/")
|
||||
if len(parts) != 2 {
|
||||
r.Log.Info("non well-formed Resource Limit annotation", "key", k)
|
||||
r.Log.V(4).Info("non well-formed Resource Limit annotation", "key", k)
|
||||
|
||||
continue
|
||||
}
|
||||
@@ -43,14 +43,14 @@ func (r *Manager) syncCustomResourceQuotaUsages(ctx context.Context, tenant *cap
|
||||
parts = strings.Split(parts[1], "_")
|
||||
|
||||
if len(parts) != 2 {
|
||||
r.Log.Info("non well-formed Resource Limit annotation, cannot retrieve version", "key", k)
|
||||
r.Log.V(4).Info("non well-formed Resource Limit annotation, cannot retrieve version", "key", k)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
groupKindParts := strings.Split(parts[0], ".")
|
||||
if len(groupKindParts) < 2 {
|
||||
r.Log.Info("non well-formed Resource Limit annotation, cannot retrieve kind and group", "key", k)
|
||||
r.Log.V(4).Info("non well-formed Resource Limit annotation, cannot retrieve kind and group", "key", k)
|
||||
|
||||
continue
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func (r *Manager) syncCustomResourceQuotaUsages(ctx context.Context, tenant *cap
|
||||
err := retry.RetryOnConflict(retry.DefaultBackoff, func() (retryErr error) {
|
||||
tnt := &capsulev1beta2.Tenant{}
|
||||
if retryErr = r.Get(ctx, types.NamespacedName{Name: tenant.GetName()}, tnt); retryErr != nil {
|
||||
return
|
||||
return retryErr
|
||||
}
|
||||
|
||||
if tnt.GetAnnotations() == nil {
|
||||
@@ -123,7 +123,7 @@ func (r *Manager) syncCustomResourceQuotaUsages(ctx context.Context, tenant *cap
|
||||
usedMap[key] += used
|
||||
}
|
||||
|
||||
return
|
||||
return scopeErr
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ func (r *Manager) ownerClusterRoleBindings(owner capsulev1beta2.OwnerSpec, clust
|
||||
Subjects: []rbacv1.Subject{
|
||||
subject,
|
||||
},
|
||||
Labels: owner.Labels,
|
||||
Annotations: owner.Annotations,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,20 +93,19 @@ func (r *Manager) syncRoleBindings(ctx context.Context, tenant *capsulev1beta2.T
|
||||
return group.Wait()
|
||||
}
|
||||
|
||||
//nolint:nakedret
|
||||
func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsulev1beta2.Tenant, ns string, keys []string, hashFn func(binding api.AdditionalRoleBindingsSpec) string) (err error) {
|
||||
var tenantLabel, roleBindingLabel string
|
||||
|
||||
if tenantLabel, err = utils.GetTypeLabel(&capsulev1beta2.Tenant{}); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if roleBindingLabel, err = utils.GetTypeLabel(&rbacv1.RoleBinding{}); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if err = r.pruningResources(ctx, ns, keys, &rbacv1.RoleBinding{}); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
var roleBindings []api.AdditionalRoleBindingsSpec
|
||||
@@ -130,17 +131,26 @@ func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsule
|
||||
var res controllerutil.OperationResult
|
||||
|
||||
res, err = controllerutil.CreateOrUpdate(ctx, r.Client, target, func() error {
|
||||
if target.Labels == nil {
|
||||
target.Labels = map[string]string{}
|
||||
target.Labels = map[string]string{}
|
||||
target.Annotations = map[string]string{}
|
||||
|
||||
if roleBinding.Labels != nil {
|
||||
target.Labels = roleBinding.Labels
|
||||
}
|
||||
|
||||
target.Labels[tenantLabel] = tenant.Name
|
||||
target.Labels[roleBindingLabel] = roleBindingHashLabel
|
||||
|
||||
if roleBinding.Annotations != nil {
|
||||
target.Annotations = roleBinding.Annotations
|
||||
}
|
||||
|
||||
target.RoleRef = rbacv1.RoleRef{
|
||||
APIGroup: rbacv1.GroupName,
|
||||
Kind: "ClusterRole",
|
||||
Name: roleBinding.ClusterRoleName,
|
||||
}
|
||||
|
||||
target.Subjects = roleBinding.Subjects
|
||||
|
||||
return controllerutil.SetControllerReference(tenant, target, r.Scheme())
|
||||
@@ -152,10 +162,10 @@ func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsule
|
||||
r.Log.Error(err, "Cannot sync RoleBinding")
|
||||
}
|
||||
|
||||
r.Log.Info(fmt.Sprintf("RoleBinding sync result: %s", string(res)), "name", target.Name, "namespace", target.Namespace)
|
||||
r.Log.V(4).Info(fmt.Sprintf("RoleBinding sync result: %s", string(res)), "name", target.Name, "namespace", target.Namespace)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/pkg/utils"
|
||||
"github.com/projectcapsule/capsule/pkg/utils"
|
||||
)
|
||||
|
||||
// pruningResources is taking care of removing the no more requested sub-resources as LimitRange, ResourceQuota or
|
||||
@@ -22,8 +22,8 @@ import (
|
||||
func (r *Manager) pruningResources(ctx context.Context, ns string, keys []string, obj client.Object) (err error) {
|
||||
var capsuleLabel string
|
||||
|
||||
if capsuleLabel, err = capsulev1beta2.GetTypeLabel(obj); err != nil {
|
||||
return
|
||||
if capsuleLabel, err = utils.GetTypeLabel(obj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selector := labels.NewSelector()
|
||||
@@ -31,7 +31,7 @@ func (r *Manager) pruningResources(ctx context.Context, ns string, keys []string
|
||||
var exists *labels.Requirement
|
||||
|
||||
if exists, err = labels.NewRequirement(capsuleLabel, selection.Exists, []string{}); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
selector = selector.Add(*exists)
|
||||
@@ -46,7 +46,7 @@ func (r *Manager) pruningResources(ctx context.Context, ns string, keys []string
|
||||
selector = selector.Add(*notIn)
|
||||
}
|
||||
|
||||
r.Log.Info("Pruning objects with label selector " + selector.String())
|
||||
r.Log.V(3).Info("Pruning objects with label selector " + selector.String())
|
||||
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
return r.DeleteAllOf(ctx, obj, &client.DeleteAllOfOptions{
|
||||
|
||||
@@ -76,7 +76,7 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
|
||||
func (r Reconciler) ReconcileCertificates(ctx context.Context, certSecret *corev1.Secret) error {
|
||||
if r.shouldUpdateCertificate(certSecret) {
|
||||
r.Log.Info("Generating new TLS certificate")
|
||||
r.Log.V(3).Info("Generating new TLS certificate")
|
||||
|
||||
ca, err := cert.GenerateCertificateAuthority()
|
||||
if err != nil {
|
||||
@@ -122,7 +122,7 @@ func (r Reconciler) ReconcileCertificates(ctx context.Context, certSecret *corev
|
||||
return fmt.Errorf("missing %s field in %s secret", corev1.ServiceAccountRootCAKey, r.Configuration.TLSSecretName())
|
||||
}
|
||||
|
||||
r.Log.Info("Updating caBundle in webhooks and crd")
|
||||
r.Log.V(4).Info("Updating caBundle in webhooks and crd")
|
||||
|
||||
group := new(errgroup.Group)
|
||||
group.Go(func() error {
|
||||
@@ -149,7 +149,7 @@ func (r Reconciler) ReconcileCertificates(ctx context.Context, certSecret *corev
|
||||
return err
|
||||
}
|
||||
|
||||
r.Log.Info("Updating capsule operator pods")
|
||||
r.Log.V(4).Info("Updating capsule operator pods")
|
||||
|
||||
for _, pod := range operatorPods.Items {
|
||||
p := pod
|
||||
@@ -189,7 +189,7 @@ func (r Reconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.R
|
||||
requeueTime := certificate.NotAfter.Add(-(certificateExpirationThreshold - 1*time.Second))
|
||||
rq := requeueTime.Sub(now)
|
||||
|
||||
r.Log.Info("Reconciliation completed, processing back in " + rq.String())
|
||||
r.Log.V(4).Info("Reconciliation completed, processing back in " + rq.String())
|
||||
|
||||
return reconcile.Result{Requeue: true, RequeueAfter: rq}, nil
|
||||
}
|
||||
@@ -210,7 +210,7 @@ func (r Reconciler) shouldUpdateCertificate(secret *corev1.Secret) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
r.Log.Info("Skipping TLS certificate generation as it is still valid")
|
||||
r.Log.V(4).Info("Skipping TLS certificate generation as it is still valid")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
8
controllers/utils/options.go
Normal file
8
controllers/utils/options.go
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package utils
|
||||
|
||||
type ControllerOptions struct {
|
||||
MaxConcurrentReconciles int
|
||||
}
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
@@ -23,7 +25,9 @@ type Patch struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
var _ = Describe("enforcing a Container Registry", Label("tenant", "images", "registry"), func() {
|
||||
originConfig := &capsulev1beta2.CapsuleConfiguration{}
|
||||
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "container-registry",
|
||||
@@ -43,13 +47,27 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: defaultConfigurationName}, originConfig)).To(Succeed())
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
tnt.ResourceVersion = ""
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
|
||||
|
||||
// Restore Configuration
|
||||
Eventually(func() error {
|
||||
c := &capsulev1beta2.CapsuleConfiguration{}
|
||||
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: originConfig.Name}, c); err != nil {
|
||||
return err
|
||||
}
|
||||
// Apply the initial configuration from originConfig to c
|
||||
c.Spec = originConfig.Spec
|
||||
return k8sClient.Update(context.Background(), c)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
})
|
||||
|
||||
It("should add labels to Namespace", func() {
|
||||
@@ -71,7 +89,6 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
|
||||
It("should deny running a gcr.io container", func() {
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -86,14 +103,21 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cs := ownerClient(tnt.Spec.Owners[0])
|
||||
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
|
||||
Expect(err).ShouldNot(Succeed())
|
||||
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
|
||||
|
||||
return err
|
||||
}).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
It("should allow using a registry only match", func() {
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -110,10 +134,26 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
}
|
||||
|
||||
cs := ownerClient(tnt.Spec.Owners[0])
|
||||
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
|
||||
|
||||
return err
|
||||
}).Should(Succeed())
|
||||
|
||||
By("verifying the image was correctly mutated", func() {
|
||||
created := &corev1.Pod{}
|
||||
Expect(k8sClient.Get(context.Background(), types.NamespacedName{
|
||||
Namespace: ns.Name,
|
||||
Name: pod.Name,
|
||||
}, created)).To(Succeed())
|
||||
|
||||
Expect(created.Spec.Containers).To(HaveLen(1))
|
||||
Expect(created.Spec.Containers[0].Image).To(Equal("myregistry.azurecr.io/myapp:latest"))
|
||||
})
|
||||
})
|
||||
|
||||
It("should deny patching a not matching registry after applying with a matching (Container)", func() {
|
||||
@@ -144,6 +184,17 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
return err
|
||||
}).Should(Succeed())
|
||||
|
||||
By("verifying the image was correctly mutated", func() {
|
||||
created := &corev1.Pod{}
|
||||
Expect(k8sClient.Get(context.Background(), types.NamespacedName{
|
||||
Namespace: ns.Name,
|
||||
Name: pod.Name,
|
||||
}, created)).To(Succeed())
|
||||
|
||||
Expect(created.Spec.Containers).To(HaveLen(1))
|
||||
Expect(created.Spec.Containers[0].Image).To(Equal("myregistry.azurecr.io/myapp:latest"))
|
||||
})
|
||||
|
||||
Eventually(func() error {
|
||||
payload := []Patch{{
|
||||
Op: "replace",
|
||||
@@ -159,6 +210,89 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
}).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
It("should deny patching a not matching registry after applying with a matching (EphemeralContainer)", func() {
|
||||
ns := NewNamespace("")
|
||||
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "container",
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "container",
|
||||
Image: "docker.io/google-containers/pause-amd64:3.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cs := ownerClient(tnt.Spec.Owners[0])
|
||||
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
role := &rbacv1.Role{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ephemeralcontainers-editor",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""}, // core API group
|
||||
Resources: []string{"pods/ephemeralcontainers"},
|
||||
Verbs: []string{"update", "patch"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rb := &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ephemeralcontainers-editor-binding",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: rbacv1.UserKind,
|
||||
Name: tnt.Spec.Owners[0].Name,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Role",
|
||||
Name: role.Name,
|
||||
},
|
||||
}
|
||||
|
||||
// Create role and binding before test logic
|
||||
Expect(k8sClient.Create(context.TODO(), role)).To(Succeed())
|
||||
Expect(k8sClient.Create(context.TODO(), rb)).To(Succeed())
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
|
||||
|
||||
return err
|
||||
}).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg",
|
||||
Image: "attacker/google-containers/pause-amd64:3.0",
|
||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
It("should deny patching a not matching registry after applying with a matching (initContainer)", func() {
|
||||
ns := NewNamespace("")
|
||||
|
||||
@@ -208,7 +342,50 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
}).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
It("should allow patching a matching registry after applying with a matching (Container)", func() {
|
||||
It("should deny patching a not matching registry after applying with a matching (Container)", func() {
|
||||
ns := NewNamespace("")
|
||||
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "container",
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "container",
|
||||
Image: "myregistry.azurecr.io/myapp:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cs := ownerClient(tnt.Spec.Owners[0])
|
||||
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
|
||||
|
||||
return err
|
||||
}).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
payload := []Patch{{
|
||||
Op: "replace",
|
||||
Path: "/spec/initContainers/0/image",
|
||||
Value: "attacker/google-containers/pause-amd64:3.0",
|
||||
}}
|
||||
payloadBytes, _ := json.Marshal(payload)
|
||||
_, err := cs.CoreV1().Pods(ns.GetName()).Patch(context.TODO(), pod.GetName(), types.JSONPatchType, payloadBytes, metav1.PatchOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
It("should allow patching a matching registry after applying with a matching (EphemeralContainer)", func() {
|
||||
ns := NewNamespace("")
|
||||
|
||||
pod := &corev1.Pod{
|
||||
@@ -230,6 +407,42 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
role := &rbacv1.Role{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ephemeralcontainers-editor",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""}, // core API group
|
||||
Resources: []string{"pods/ephemeralcontainers"},
|
||||
Verbs: []string{"update", "patch"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rb := &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ephemeralcontainers-editor-binding",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: rbacv1.UserKind,
|
||||
Name: tnt.Spec.Owners[0].Name,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Role",
|
||||
Name: role.Name,
|
||||
},
|
||||
}
|
||||
|
||||
// Create role and binding before test logic
|
||||
Expect(k8sClient.Create(context.TODO(), role)).To(Succeed())
|
||||
Expect(k8sClient.Create(context.TODO(), rb)).To(Succeed())
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
_, err := cs.CoreV1().Pods(ns.Name).Create(context.Background(), pod, metav1.CreateOptions{})
|
||||
|
||||
@@ -237,13 +450,17 @@ var _ = Describe("enforcing a Container Registry", Label("tenant"), func() {
|
||||
}).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
payload := []Patch{{
|
||||
Op: "replace",
|
||||
Path: "/spec/containers/0/image",
|
||||
Value: "myregistry.azurecr.io/google-containers/pause-amd64:3.1",
|
||||
}}
|
||||
payloadBytes, _ := json.Marshal(payload)
|
||||
_, err := cs.CoreV1().Pods(ns.GetName()).Patch(context.TODO(), pod.GetName(), types.JSONPatchType, payloadBytes, metav1.PatchOptions{})
|
||||
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg",
|
||||
Image: "myregistry.azurecr.io/google-containers/pause-amd64:3.1",
|
||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@ import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
)
|
||||
|
||||
var _ = Describe("enforcing some defined ImagePullPolicy", Label("pod"), func() {
|
||||
var _ = Describe("enforcing some defined ImagePullPolicy", Label("tenant", "images", "policy"), func() {
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "image-pull-policies",
|
||||
@@ -48,6 +49,42 @@ var _ = Describe("enforcing some defined ImagePullPolicy", Label("pod"), func()
|
||||
|
||||
cs := ownerClient(tnt.Spec.Owners[0])
|
||||
|
||||
role := &rbacv1.Role{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ephemeralcontainers-editor",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""}, // core API group
|
||||
Resources: []string{"pods/ephemeralcontainers"},
|
||||
Verbs: []string{"update", "patch"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rb := &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ephemeralcontainers-editor-binding",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: rbacv1.UserKind,
|
||||
Name: tnt.Spec.Owners[0].Name,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Role",
|
||||
Name: role.Name,
|
||||
},
|
||||
}
|
||||
|
||||
// Create role and binding before test logic
|
||||
Expect(k8sClient.Create(context.TODO(), role)).To(Succeed())
|
||||
Expect(k8sClient.Create(context.TODO(), rb)).To(Succeed())
|
||||
|
||||
By("allowing Always", func() {
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -69,6 +106,25 @@ var _ = Describe("enforcing some defined ImagePullPolicy", Label("pod"), func()
|
||||
|
||||
return
|
||||
}).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg",
|
||||
Image: "gcr.io/google_containers/pause-amd64:3.0",
|
||||
ImagePullPolicy: corev1.PullAlways,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}).Should(Succeed())
|
||||
|
||||
})
|
||||
|
||||
By("allowing IfNotPresent", func() {
|
||||
@@ -92,6 +148,24 @@ var _ = Describe("enforcing some defined ImagePullPolicy", Label("pod"), func()
|
||||
|
||||
return
|
||||
}).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg",
|
||||
Image: "gcr.io/google_containers/pause-amd64:3.0",
|
||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}).Should(Succeed())
|
||||
})
|
||||
|
||||
By("blocking Never", func() {
|
||||
@@ -115,6 +189,25 @@ var _ = Describe("enforcing some defined ImagePullPolicy", Label("pod"), func()
|
||||
|
||||
return
|
||||
}).ShouldNot(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg",
|
||||
Image: "gcr.io/google_containers/pause-amd64:3.0",
|
||||
ImagePullPolicy: corev1.PullNever,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}).ShouldNot(Succeed())
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,13 +9,14 @@ import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
)
|
||||
|
||||
var _ = Describe("enforcing a defined ImagePullPolicy", Label("pod"), func() {
|
||||
var _ = Describe("enforcing a defined ImagePullPolicy", Label("tenant", "images", "policy"), func() {
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "image-pull-policy",
|
||||
@@ -48,6 +49,42 @@ var _ = Describe("enforcing a defined ImagePullPolicy", Label("pod"), func() {
|
||||
|
||||
cs := ownerClient(tnt.Spec.Owners[0])
|
||||
|
||||
role := &rbacv1.Role{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ephemeralcontainers-editor",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""}, // core API group
|
||||
Resources: []string{"pods/ephemeralcontainers"},
|
||||
Verbs: []string{"update", "patch"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rb := &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ephemeralcontainers-editor-binding",
|
||||
Namespace: ns.GetName(),
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: rbacv1.UserKind,
|
||||
Name: tnt.Spec.Owners[0].Name,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Role",
|
||||
Name: role.Name,
|
||||
},
|
||||
}
|
||||
|
||||
// Create role and binding before test logic
|
||||
Expect(k8sClient.Create(context.TODO(), role)).To(Succeed())
|
||||
Expect(k8sClient.Create(context.TODO(), rb)).To(Succeed())
|
||||
|
||||
By("allowing Always", func() {
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -69,6 +106,24 @@ var _ = Describe("enforcing a defined ImagePullPolicy", Label("pod"), func() {
|
||||
|
||||
return
|
||||
}).Should(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg",
|
||||
Image: "gcr.io/google_containers/pause-amd64:3.0",
|
||||
ImagePullPolicy: corev1.PullAlways,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}).Should(Succeed())
|
||||
})
|
||||
|
||||
By("blocking IfNotPresent", func() {
|
||||
@@ -92,6 +147,24 @@ var _ = Describe("enforcing a defined ImagePullPolicy", Label("pod"), func() {
|
||||
|
||||
return
|
||||
}).ShouldNot(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg",
|
||||
Image: "gcr.io/google_containers/pause-amd64:3.0",
|
||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
By("blocking Never", func() {
|
||||
@@ -115,6 +188,24 @@ var _ = Describe("enforcing a defined ImagePullPolicy", Label("pod"), func() {
|
||||
|
||||
return
|
||||
}).ShouldNot(Succeed())
|
||||
|
||||
Eventually(func() error {
|
||||
pod.Spec.EphemeralContainers = []corev1.EphemeralContainer{
|
||||
{
|
||||
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
|
||||
Name: "dbg",
|
||||
Image: "gcr.io/google_containers/pause-amd64:3.0",
|
||||
ImagePullPolicy: corev1.PullNever,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := cs.CoreV1().Pods(ns.Name).UpdateEphemeralContainers(context.Background(), pod.Name, pod, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}).ShouldNot(Succeed())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,14 +8,17 @@ import (
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
)
|
||||
|
||||
var _ = Describe("creating a Namespace for a Tenant with additional metadata", Label("namespace"), func() {
|
||||
var _ = Describe("creating a Namespace for a Tenant with additional metadata", Label("namespace", "metadata"), func() {
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-metadata",
|
||||
@@ -35,7 +38,16 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
|
||||
Kind: "User",
|
||||
},
|
||||
},
|
||||
NamespaceOptions: &capsulev1beta2.NamespaceOptions{
|
||||
},
|
||||
}
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
It("should contain additional Namespace metadata", func() {
|
||||
By("prepare tenant", func() {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
AdditionalMetadata: &api.AdditionalMetadataSpec{
|
||||
Labels: map[string]string{
|
||||
"k8s.io/custom-label": "foo",
|
||||
@@ -48,20 +60,16 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
|
||||
"clastix.io/custom-annotation": "buzz",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
EventuallyCreation(func() error {
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
})
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
|
||||
})
|
||||
EventuallyCreation(func() error {
|
||||
tnt.ResourceVersion = ""
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
It("should contain additional Namespace metadata", func() {
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
@@ -105,29 +113,10 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("creating a Namespace for a Tenant with additional metadata list", func() {
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-metadata",
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: "cap",
|
||||
Kind: "dummy",
|
||||
Name: "tenant-metadata",
|
||||
UID: "tenant-metadata",
|
||||
},
|
||||
},
|
||||
},
|
||||
Spec: capsulev1beta2.TenantSpec{
|
||||
Owners: capsulev1beta2.OwnerListSpec{
|
||||
{
|
||||
Name: "gatsby",
|
||||
Kind: "User",
|
||||
},
|
||||
},
|
||||
NamespaceOptions: &capsulev1beta2.NamespaceOptions{
|
||||
It("should contain additional Namespace metadata", func() {
|
||||
By("prepare tenant", func() {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{
|
||||
{
|
||||
Labels: map[string]string{
|
||||
@@ -184,20 +173,16 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata lis
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
EventuallyCreation(func() error {
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
})
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
|
||||
})
|
||||
EventuallyCreation(func() error {
|
||||
tnt.ResourceVersion = ""
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
It("should contain additional Namespace metadata", func() {
|
||||
labels := map[string]string{
|
||||
"matching_namespace_label": "matching_namespace_label_value",
|
||||
}
|
||||
@@ -295,6 +280,434 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata lis
|
||||
return
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
It("should contain additional Namespace metadata", func() {
|
||||
By("prepare tenant", func() {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
ManagedMetadataOnly: false,
|
||||
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{
|
||||
{
|
||||
Labels: map[string]string{
|
||||
"clastix.io/custom-label": "bar",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"clastix.io/custom-annotation": "buzz",
|
||||
},
|
||||
},
|
||||
{
|
||||
Labels: map[string]string{
|
||||
"k8s.io/custom-label": "foo",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"k8s.io/custom-annotation": "bizz",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
tnt.ResourceVersion = ""
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
labels := map[string]string{
|
||||
"matching_namespace_label": "matching_namespace_label_value",
|
||||
}
|
||||
|
||||
ns := NewNamespace("", labels)
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
By("checking additional labels", func() {
|
||||
Eventually(func() (ok bool) {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
|
||||
for _, mv := range tnt.Spec.NamespaceOptions.AdditionalMetadataList {
|
||||
for k, v := range mv.Labels {
|
||||
if k == "capsule.clastix.io/tenant" || k == "kubernetes.io/metadata.name" {
|
||||
continue // this label is managed and shouldn't be set by the user
|
||||
}
|
||||
if ok, _ = HaveKeyWithValue(k, v).Match(ns.Labels); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
|
||||
})
|
||||
By("checking managed labels", func() {
|
||||
Eventually(func() (ok bool) {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
|
||||
if ok, _ = HaveKeyWithValue("capsule.clastix.io/tenant", tnt.GetName()).Match(ns.Labels); !ok {
|
||||
return
|
||||
}
|
||||
if ok, _ = HaveKeyWithValue("kubernetes.io/metadata.name", ns.GetName()).Match(ns.Labels); !ok {
|
||||
return
|
||||
}
|
||||
return
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
|
||||
})
|
||||
|
||||
By("checking additional annotations", func() {
|
||||
Eventually(func() (ok bool) {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
|
||||
for _, mv := range tnt.Spec.NamespaceOptions.AdditionalMetadataList {
|
||||
for k, v := range mv.Annotations {
|
||||
if ok, _ = HaveKeyWithValue(k, v).Match(ns.Annotations); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
|
||||
})
|
||||
|
||||
By("patching labels and annotations on the Namespace", func() {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).To(Succeed())
|
||||
|
||||
before := ns.DeepCopy()
|
||||
ns.Labels["test-label"] = "test-value"
|
||||
ns.Labels["k8s.io/custom-label"] = "foo-value"
|
||||
ns.Annotations["test-annotation"] = "test-value"
|
||||
ns.Annotations["k8s.io/custom-annotation"] = "bizz-value"
|
||||
|
||||
Expect(k8sClient.Patch(context.TODO(), ns, client.MergeFrom(before))).To(Succeed())
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
By("Add additional annotations (Tenant Owner)", func() {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
|
||||
|
||||
expectedLabels := map[string]string{
|
||||
"test-label": "test-value",
|
||||
"clastix.io/custom-label": "bar",
|
||||
"k8s.io/custom-label": "foo",
|
||||
"matching_namespace_label": "matching_namespace_label_value",
|
||||
"capsule.clastix.io/tenant": tnt.GetName(),
|
||||
"kubernetes.io/metadata.name": ns.GetName(),
|
||||
}
|
||||
|
||||
Eventually(func() map[string]string {
|
||||
got := &corev1.Namespace{}
|
||||
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
|
||||
return nil
|
||||
}
|
||||
ann := got.GetLabels()
|
||||
if ann == nil {
|
||||
ann = map[string]string{}
|
||||
}
|
||||
return ann
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedLabels))
|
||||
|
||||
expectedAnnotations := map[string]string{
|
||||
"test-annotation": "test-value",
|
||||
"clastix.io/custom-annotation": "buzz",
|
||||
"k8s.io/custom-annotation": "bizz",
|
||||
}
|
||||
Eventually(func() map[string]string {
|
||||
got := &corev1.Namespace{}
|
||||
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
|
||||
return nil
|
||||
}
|
||||
ann := got.GetAnnotations()
|
||||
if ann == nil {
|
||||
ann = map[string]string{}
|
||||
}
|
||||
return ann
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedAnnotations))
|
||||
|
||||
By("verify tenant status", func() {
|
||||
condition := tnt.Status.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected tenant condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected tenant condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected tenant condition reason to be Succeeded")
|
||||
})
|
||||
|
||||
By("verify namespace status", func() {
|
||||
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns.GetName(), UID: ns.GetUID()})
|
||||
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
|
||||
|
||||
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(instance.Name).To(Equal(ns.GetName()))
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected namespace condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
|
||||
|
||||
expectedMetadata := &capsulev1beta2.TenantStatusNamespaceMetadata{
|
||||
Labels: map[string]string{
|
||||
"clastix.io/custom-label": "bar",
|
||||
"k8s.io/custom-label": "foo",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"clastix.io/custom-annotation": "buzz",
|
||||
"k8s.io/custom-annotation": "bizz",
|
||||
},
|
||||
}
|
||||
|
||||
Expect(instance.Metadata).To(Equal(expectedMetadata))
|
||||
})
|
||||
})
|
||||
|
||||
By("change managed additional metadata", func() {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
ManagedMetadataOnly: false,
|
||||
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{
|
||||
{
|
||||
Labels: map[string]string{
|
||||
"clastix.io/custom-label": "bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
Annotations: map[string]string{
|
||||
"k8s.io/custom-annotation": "bizz",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
By("verify metadata lifecycle (valid update)", func() {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).To(Succeed())
|
||||
|
||||
expectedLabels := map[string]string{
|
||||
"test-label": "test-value",
|
||||
"clastix.io/custom-label": "bar",
|
||||
"matching_namespace_label": "matching_namespace_label_value",
|
||||
"capsule.clastix.io/tenant": tnt.GetName(),
|
||||
"kubernetes.io/metadata.name": ns.GetName(),
|
||||
}
|
||||
Eventually(func() map[string]string {
|
||||
got := &corev1.Namespace{}
|
||||
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
|
||||
return nil
|
||||
}
|
||||
ann := got.GetLabels()
|
||||
if ann == nil {
|
||||
ann = map[string]string{}
|
||||
}
|
||||
return ann
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedLabels))
|
||||
|
||||
expectedAnnotations := map[string]string{
|
||||
"test-annotation": "test-value",
|
||||
"k8s.io/custom-annotation": "bizz",
|
||||
}
|
||||
Eventually(func() map[string]string {
|
||||
got := &corev1.Namespace{}
|
||||
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
|
||||
return nil
|
||||
}
|
||||
ann := got.GetAnnotations()
|
||||
if ann == nil {
|
||||
ann = map[string]string{}
|
||||
}
|
||||
return ann
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedAnnotations))
|
||||
|
||||
By("verify tenant status", func() {
|
||||
condition := tnt.Status.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected tenant condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected tenant condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected tenant condition reason to be Succeeded")
|
||||
})
|
||||
|
||||
By("verify namespace status", func() {
|
||||
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns.GetName(), UID: ns.GetUID()})
|
||||
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
|
||||
|
||||
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(instance.Name).To(Equal(ns.GetName()))
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected namespace condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
|
||||
|
||||
expectedMetadata := &capsulev1beta2.TenantStatusNamespaceMetadata{
|
||||
Labels: map[string]string{
|
||||
"clastix.io/custom-label": "bar",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"k8s.io/custom-annotation": "bizz",
|
||||
},
|
||||
}
|
||||
|
||||
Expect(instance.Metadata).To(Equal(expectedMetadata))
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
By("change managed additional metadata (provoke an error)", func() {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
ManagedMetadataOnly: false,
|
||||
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{
|
||||
{
|
||||
Labels: map[string]string{
|
||||
"clastix.io???custom-label": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
By("verify metadata lifecycle (faulty update)", func() {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).To(Succeed())
|
||||
|
||||
expectedLabels := map[string]string{
|
||||
"test-label": "test-value",
|
||||
"clastix.io/custom-label": "bar",
|
||||
"matching_namespace_label": "matching_namespace_label_value",
|
||||
"capsule.clastix.io/tenant": tnt.GetName(),
|
||||
"kubernetes.io/metadata.name": ns.GetName(),
|
||||
}
|
||||
Eventually(func() map[string]string {
|
||||
got := &corev1.Namespace{}
|
||||
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
|
||||
return nil
|
||||
}
|
||||
ann := got.GetLabels()
|
||||
if ann == nil {
|
||||
ann = map[string]string{}
|
||||
}
|
||||
return ann
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedLabels))
|
||||
|
||||
expectedAnnotations := map[string]string{
|
||||
"test-annotation": "test-value",
|
||||
"k8s.io/custom-annotation": "bizz",
|
||||
}
|
||||
Eventually(func() map[string]string {
|
||||
got := &corev1.Namespace{}
|
||||
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
|
||||
return nil
|
||||
}
|
||||
ann := got.GetAnnotations()
|
||||
if ann == nil {
|
||||
ann = map[string]string{}
|
||||
}
|
||||
return ann
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedAnnotations))
|
||||
|
||||
By("verify tenant status", func() {
|
||||
condition := tnt.Status.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionFalse), "Expected tenant condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected tenant condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.FailedReason), "Expected tenant condition reason to be Succeeded")
|
||||
})
|
||||
|
||||
By("verify namespace status", func() {
|
||||
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns.GetName(), UID: ns.GetUID()})
|
||||
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
|
||||
|
||||
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(instance.Name).To(Equal(ns.GetName()))
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionFalse), "Expected namespace condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.FailedReason), "Expected namespace condition reason to be Succeeded")
|
||||
|
||||
expectedMetadata := &capsulev1beta2.TenantStatusNamespaceMetadata{
|
||||
Labels: map[string]string{
|
||||
"clastix.io/custom-label": "bar",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"k8s.io/custom-annotation": "bizz",
|
||||
},
|
||||
}
|
||||
|
||||
Expect(instance.Metadata).To(Equal(expectedMetadata))
|
||||
})
|
||||
})
|
||||
|
||||
By("change managed additional metadata (empty update)", func() {
|
||||
tnt.Spec.NamespaceOptions = &capsulev1beta2.NamespaceOptions{
|
||||
ManagedMetadataOnly: false,
|
||||
AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{},
|
||||
}
|
||||
|
||||
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
By("verify metadata lifecycle (empty update)", func() {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).To(Succeed())
|
||||
|
||||
expectedLabels := map[string]string{
|
||||
"test-label": "test-value",
|
||||
"matching_namespace_label": "matching_namespace_label_value",
|
||||
"capsule.clastix.io/tenant": tnt.GetName(),
|
||||
"kubernetes.io/metadata.name": ns.GetName(),
|
||||
}
|
||||
Eventually(func() map[string]string {
|
||||
got := &corev1.Namespace{}
|
||||
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
|
||||
return nil
|
||||
}
|
||||
ann := got.GetLabels()
|
||||
if ann == nil {
|
||||
ann = map[string]string{}
|
||||
}
|
||||
return ann
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedLabels))
|
||||
|
||||
expectedAnnotations := map[string]string{
|
||||
"test-annotation": "test-value",
|
||||
}
|
||||
Eventually(func() map[string]string {
|
||||
got := &corev1.Namespace{}
|
||||
if err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, got); err != nil {
|
||||
return nil
|
||||
}
|
||||
ann := got.GetAnnotations()
|
||||
if ann == nil {
|
||||
ann = map[string]string{}
|
||||
}
|
||||
return ann
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expectedAnnotations))
|
||||
|
||||
By("verify tenant status", func() {
|
||||
condition := tnt.Status.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected tenant condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected tenant condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected tenant condition reason to be Succeeded")
|
||||
})
|
||||
|
||||
By("verify namespace status", func() {
|
||||
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns.GetName(), UID: ns.GetUID()})
|
||||
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
|
||||
|
||||
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(instance.Name).To(Equal(ns.GetName()))
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected namespace condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
|
||||
|
||||
expectedMetadata := &capsulev1beta2.TenantStatusNamespaceMetadata{}
|
||||
Expect(instance.Metadata).To(Equal(expectedMetadata))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
var _ = Describe("creating a Namespace for a Tenant with additional metadata", Label("namespace"), func() {
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-metadata",
|
||||
Name: "tenant-metadata-controller",
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: "cap",
|
||||
@@ -68,6 +68,7 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
|
||||
Expect(ns.Labels).ShouldNot(HaveKeyWithValue("newlabel", "foobazbar"))
|
||||
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
tnt.Spec.NamespaceOptions.AdditionalMetadata.Labels["newlabel"] = "foobazbar"
|
||||
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
|
||||
|
||||
@@ -81,6 +82,7 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
|
||||
Expect(ns.Labels).ShouldNot(HaveKeyWithValue("newannotation", "foobazbar"))
|
||||
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, tnt)).Should(Succeed())
|
||||
tnt.Spec.NamespaceOptions.AdditionalMetadata.Annotations["newannotation"] = "foobazbar"
|
||||
Expect(k8sClient.Update(context.TODO(), tnt)).Should(Succeed())
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
var _ = Describe("creating a Namespace for a Tenant with additional metadata", Label("namespace"), func() {
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-metadata",
|
||||
Name: "tenant-metadata-webhook",
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: "cap",
|
||||
|
||||
112
e2e/namespace_status_test.go
Normal file
112
e2e/namespace_status_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright 2020-2023 Project Capsule Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
)
|
||||
|
||||
var _ = Describe("creating namespace with status lifecycle", Label("namespace", "status"), func() {
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-status",
|
||||
},
|
||||
Spec: capsulev1beta2.TenantSpec{
|
||||
Owners: capsulev1beta2.OwnerListSpec{
|
||||
{
|
||||
Name: "gatsby",
|
||||
Kind: "User",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
EventuallyCreation(func() error {
|
||||
tnt.ResourceVersion = ""
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
})
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
It("verify namespace lifecycle (functionality)", func() {
|
||||
ns1 := NewNamespace("")
|
||||
By("creating first namespace", func() {
|
||||
NamespaceCreation(ns1, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElements(ns1.GetName()))
|
||||
|
||||
t := &capsulev1beta2.Tenant{}
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
|
||||
|
||||
Expect(tnt.Status.Size).To(Equal(uint(1)))
|
||||
|
||||
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns1.GetName(), UID: ns1.GetUID()})
|
||||
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
|
||||
|
||||
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(instance.Name).To(Equal(ns1.GetName()))
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected namespace condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
|
||||
})
|
||||
|
||||
ns2 := NewNamespace("")
|
||||
By("creating second namespace", func() {
|
||||
NamespaceCreation(ns2, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElements(ns2.GetName()))
|
||||
|
||||
t := &capsulev1beta2.Tenant{}
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
|
||||
|
||||
Expect(tnt.Status.Size).To(Equal(uint(2)))
|
||||
|
||||
instance := tnt.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns2.GetName(), UID: ns2.GetUID()})
|
||||
Expect(instance).NotTo(BeNil(), "Namespace instance should not be nil")
|
||||
|
||||
condition := instance.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(instance.Name).To(Equal(ns2.GetName()))
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected namespace condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.ReadyCondition), "Expected namespace condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
|
||||
})
|
||||
|
||||
By("removing first namespace", func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), ns1)).Should(Succeed())
|
||||
|
||||
t := &capsulev1beta2.Tenant{}
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
|
||||
|
||||
Expect(t.Status.Size).To(Equal(uint(1)))
|
||||
|
||||
instance := t.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns1.GetName(), UID: ns1.GetUID()})
|
||||
Expect(instance).To(BeNil(), "Namespace instance should be nil")
|
||||
})
|
||||
|
||||
By("removing second namespace", func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), ns2)).Should(Succeed())
|
||||
|
||||
t := &capsulev1beta2.Tenant{}
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
|
||||
|
||||
Expect(t.Status.Size).To(Equal(uint(0)))
|
||||
|
||||
instance := t.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{Name: ns2.GetName(), UID: ns2.GetUID()})
|
||||
Expect(instance).To(BeNil(), "Namespace instance should be nil")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -377,6 +377,16 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() {
|
||||
Expect(claim.Status.Pool).To(Equal(expectedPool), "expected pool name to match")
|
||||
})
|
||||
|
||||
By("Error on deleting bound claim", func() {
|
||||
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
isBoundCondition(claim)
|
||||
|
||||
err = k8sClient.Delete(context.TODO(), claim)
|
||||
Expect(err).ShouldNot(Succeed())
|
||||
})
|
||||
|
||||
By("Error on patching resources for claim (Increase)", func() {
|
||||
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: claim.Name, Namespace: claim.Namespace}, claim)
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
353
e2e/sa_owner_promotion_test.go
Normal file
353
e2e/sa_owner_promotion_test.go
Normal file
@@ -0,0 +1,353 @@
|
||||
// Copyright 2020-2023 Project Capsule Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
otypes "github.com/onsi/gomega/types"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
ctrlrbac "github.com/projectcapsule/capsule/controllers/rbac"
|
||||
"github.com/projectcapsule/capsule/pkg/api"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
)
|
||||
|
||||
var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label("promotion"), func() {
|
||||
originConfig := &capsulev1beta2.CapsuleConfiguration{}
|
||||
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-owner-promotion",
|
||||
},
|
||||
Spec: capsulev1beta2.TenantSpec{
|
||||
Owners: capsulev1beta2.OwnerListSpec{
|
||||
{
|
||||
Name: "alice",
|
||||
Kind: "User",
|
||||
},
|
||||
},
|
||||
AdditionalRoleBindings: []api.AdditionalRoleBindingsSpec{
|
||||
{
|
||||
ClusterRoleName: "cluster-admin",
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Name: "default",
|
||||
},
|
||||
{
|
||||
Kind: "User",
|
||||
Name: "bob",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: defaultConfigurationName}, originConfig)).To(Succeed())
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
tnt.ResourceVersion = ""
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
})
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
|
||||
|
||||
// Restore Configuration
|
||||
Eventually(func() error {
|
||||
c := &capsulev1beta2.CapsuleConfiguration{}
|
||||
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: originConfig.Name}, c); err != nil {
|
||||
return err
|
||||
}
|
||||
// Apply the initial configuration from originConfig to c
|
||||
c.Spec = originConfig.Spec
|
||||
return k8sClient.Update(context.Background(), c)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
})
|
||||
|
||||
It("Deny Owner promotion even when feature is disabled", func() {
|
||||
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
|
||||
configuration.Spec.AllowServiceAccountPromotion = false
|
||||
})
|
||||
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
|
||||
// Create a ServiceAccount inside the tenant namespace
|
||||
sa := &corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-sa",
|
||||
Namespace: ns.Name,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(context.TODO(), sa)).Should(Succeed())
|
||||
|
||||
// Table of personas: client + expected result
|
||||
personas := map[string]struct {
|
||||
client client.Client
|
||||
matcher otypes.GomegaMatcher
|
||||
}{
|
||||
"owner": {client: impersonationClient(tnt.Spec.Owners[0].Name, withDefaultGroups(make([]string, 0)), k8sClient.Scheme()), matcher: Not(Succeed())},
|
||||
"rb-user": {client: impersonationClient("bob", withDefaultGroups(make([]string, 0)), k8sClient.Scheme()), matcher: Not(Succeed())},
|
||||
"rb-sa": {client: impersonationClient("system:serviceaccount:"+sa.GetNamespace()+":default", withDefaultGroups(make([]string, 0)), k8sClient.Scheme()), matcher: Not(Succeed())},
|
||||
}
|
||||
|
||||
for name, tc := range personas {
|
||||
By(fmt.Sprintf("trying to promote SA as %s (Setting Trigger)", name))
|
||||
|
||||
Eventually(func() error {
|
||||
saCopy := &corev1.ServiceAccount{}
|
||||
Expect(tc.client.Get(context.TODO(), client.ObjectKeyFromObject(sa), saCopy)).To(Succeed())
|
||||
|
||||
if saCopy.Labels == nil {
|
||||
saCopy.Labels = map[string]string{}
|
||||
}
|
||||
saCopy.Labels[meta.OwnerPromotionLabel] = meta.OwnerPromotionLabelTrigger
|
||||
|
||||
return tc.client.Update(context.TODO(), saCopy)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
|
||||
}
|
||||
|
||||
for name, tc := range personas {
|
||||
By(fmt.Sprintf("trying to promote SA as %s (Setting Any Value)", name))
|
||||
|
||||
Eventually(func() error {
|
||||
saCopy := &corev1.ServiceAccount{}
|
||||
Expect(tc.client.Get(context.TODO(), client.ObjectKeyFromObject(sa), saCopy)).To(Succeed())
|
||||
|
||||
if saCopy.Labels == nil {
|
||||
saCopy.Labels = map[string]string{}
|
||||
}
|
||||
saCopy.Labels[meta.OwnerPromotionLabel] = "false"
|
||||
|
||||
return tc.client.Update(context.TODO(), saCopy)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
|
||||
}
|
||||
|
||||
for name, tc := range personas {
|
||||
By(fmt.Sprintf("trying to allow deletion SA as %s (Setting Any Value)", name))
|
||||
|
||||
Eventually(func() error {
|
||||
saCopy := &corev1.ServiceAccount{}
|
||||
Expect(tc.client.Get(context.TODO(), client.ObjectKeyFromObject(sa), saCopy)).To(Succeed())
|
||||
|
||||
if saCopy.Labels == nil {
|
||||
saCopy.Labels = map[string]string{}
|
||||
}
|
||||
saCopy.Labels[meta.OwnerPromotionLabel] = "false"
|
||||
|
||||
return tc.client.Update(context.TODO(), saCopy)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
|
||||
}
|
||||
|
||||
for name, tc := range personas {
|
||||
By(fmt.Sprintf("trying to allow deletion SA as %s (Setting Any Value)", name))
|
||||
|
||||
Eventually(func() error {
|
||||
saCopy := &corev1.ServiceAccount{}
|
||||
Expect(tc.client.Get(context.TODO(), client.ObjectKeyFromObject(sa), saCopy)).To(Succeed())
|
||||
|
||||
if saCopy.Labels == nil {
|
||||
saCopy.Labels = map[string]string{}
|
||||
}
|
||||
saCopy.Labels[meta.OwnerPromotionLabel] = "false"
|
||||
|
||||
return tc.client.Update(context.TODO(), saCopy)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
|
||||
}
|
||||
})
|
||||
|
||||
It("Allow Owner promotion by Owners", func() {
|
||||
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
|
||||
configuration.Spec.AllowServiceAccountPromotion = true
|
||||
})
|
||||
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
|
||||
// Create a ServiceAccount inside the tenant namespace
|
||||
sa := &corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-sa",
|
||||
Namespace: ns.Name,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(context.TODO(), sa)).Should(Succeed())
|
||||
|
||||
// Table of personas: client + expected result
|
||||
personas := map[string]struct {
|
||||
client client.Client
|
||||
matcher otypes.GomegaMatcher
|
||||
}{
|
||||
"rb-user": {client: impersonationClient("bob", withDefaultGroups(make([]string, 0)), k8sClient.Scheme()), matcher: Not(Succeed())},
|
||||
"rb-sa": {client: impersonationClient("system:serviceaccount:"+sa.GetNamespace()+":default", withDefaultGroups(make([]string, 0)), k8sClient.Scheme()), matcher: Not(Succeed())},
|
||||
"owner": {client: impersonationClient(tnt.Spec.Owners[0].Name, withDefaultGroups(make([]string, 0)), k8sClient.Scheme()), matcher: Succeed()},
|
||||
}
|
||||
|
||||
for name, tc := range personas {
|
||||
By(fmt.Sprintf("trying to promote SA as %s (Setting Trigger)", name))
|
||||
|
||||
Eventually(func() error {
|
||||
saCopy := &corev1.ServiceAccount{}
|
||||
Expect(tc.client.Get(context.TODO(), client.ObjectKeyFromObject(sa), saCopy)).To(Succeed())
|
||||
|
||||
if saCopy.Labels == nil {
|
||||
saCopy.Labels = map[string]string{}
|
||||
}
|
||||
saCopy.Labels[meta.OwnerPromotionLabel] = meta.OwnerPromotionLabelTrigger
|
||||
|
||||
return tc.client.Update(context.TODO(), saCopy)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
|
||||
}
|
||||
|
||||
for name, tc := range personas {
|
||||
By(fmt.Sprintf("trying to promote SA as %s (Setting Generic)", name))
|
||||
|
||||
Eventually(func() error {
|
||||
saCopy := &corev1.ServiceAccount{}
|
||||
Expect(tc.client.Get(context.TODO(), client.ObjectKeyFromObject(sa), saCopy)).To(Succeed())
|
||||
|
||||
if saCopy.Labels == nil {
|
||||
saCopy.Labels = map[string]string{}
|
||||
}
|
||||
saCopy.Labels[meta.OwnerPromotionLabel] = "false"
|
||||
|
||||
return tc.client.Update(context.TODO(), saCopy)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
|
||||
}
|
||||
})
|
||||
|
||||
It("Allow Promoted ServiceAccount to interact with Tenant Namespaces", func() {
|
||||
ModifyCapsuleConfigurationOpts(func(configuration *capsulev1beta2.CapsuleConfiguration) {
|
||||
configuration.Spec.AllowServiceAccountPromotion = true
|
||||
})
|
||||
|
||||
ns := NewNamespace("")
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
|
||||
// Create a ServiceAccount inside the tenant namespace
|
||||
sa := &corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-sa",
|
||||
Namespace: ns.Name,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(context.TODO(), sa)).Should(Succeed())
|
||||
|
||||
// Table of personas: client + expected result
|
||||
personas := map[string]struct {
|
||||
client client.Client
|
||||
matcher otypes.GomegaMatcher
|
||||
}{
|
||||
"owner": {client: impersonationClient(tnt.Spec.Owners[0].Name, withDefaultGroups(make([]string, 0)), k8sClient.Scheme()), matcher: Succeed()},
|
||||
}
|
||||
|
||||
for name, tc := range personas {
|
||||
By(fmt.Sprintf("trying to promote SA as %s", name))
|
||||
|
||||
Eventually(func() error {
|
||||
saCopy := &corev1.ServiceAccount{}
|
||||
Expect(tc.client.Get(context.TODO(), client.ObjectKeyFromObject(sa), saCopy)).To(Succeed())
|
||||
|
||||
if saCopy.Labels == nil {
|
||||
saCopy.Labels = map[string]string{}
|
||||
}
|
||||
saCopy.Labels[meta.OwnerPromotionLabel] = meta.OwnerPromotionLabelTrigger
|
||||
|
||||
return tc.client.Update(context.TODO(), saCopy)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
|
||||
}
|
||||
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
|
||||
Eventually(func(g Gomega) []rbacv1.Subject {
|
||||
crb := &rbacv1.ClusterRoleBinding{}
|
||||
err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ctrlrbac.ProvisionerRoleName}, crb)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
return crb.Subjects
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(ContainElement(rbacv1.Subject{
|
||||
Kind: rbacv1.ServiceAccountKind,
|
||||
Name: "test-sa",
|
||||
Namespace: ns.Name,
|
||||
}), "expected ServiceAccount test-sa to be present in CRB subjects")
|
||||
|
||||
saClient := impersonationClient(
|
||||
fmt.Sprintf("system:serviceaccount:%s:%s", ns.Name, sa.Name),
|
||||
nil,
|
||||
k8sClient.Scheme(),
|
||||
)
|
||||
|
||||
newNs := NewNamespace("")
|
||||
Expect(saClient.Create(context.TODO(), newNs)).To(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElements(ns.GetName()))
|
||||
|
||||
Expect(saClient.Delete(context.TODO(), newNs)).To(Not(Succeed()))
|
||||
|
||||
for name, tc := range personas {
|
||||
By(fmt.Sprintf("trying to promote SA as %s", name))
|
||||
|
||||
Eventually(func() error {
|
||||
saCopy := &corev1.ServiceAccount{}
|
||||
Expect(tc.client.Get(context.TODO(), client.ObjectKeyFromObject(sa), saCopy)).To(Succeed())
|
||||
|
||||
if saCopy.Labels == nil {
|
||||
saCopy.Labels = map[string]string{}
|
||||
}
|
||||
saCopy.Labels[meta.OwnerPromotionLabel] = "false"
|
||||
|
||||
return tc.client.Update(context.TODO(), saCopy)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(tc.matcher, "persona=%s", name)
|
||||
|
||||
Eventually(func() (string, error) {
|
||||
latest := &corev1.ServiceAccount{}
|
||||
if err := k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(sa), latest); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return latest.Labels[meta.OwnerPromotionLabel], nil
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal("false"), "expected label to be set for persona=%s", name)
|
||||
|
||||
}
|
||||
|
||||
Eventually(func(g Gomega) []rbacv1.Subject {
|
||||
crb := &rbacv1.ClusterRoleBinding{}
|
||||
err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ctrlrbac.ProvisionerRoleName}, crb)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
return crb.Subjects
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Not(ContainElement(rbacv1.Subject{
|
||||
Kind: rbacv1.ServiceAccountKind,
|
||||
Name: "test-sa",
|
||||
Namespace: ns.Name,
|
||||
})), "expected ServiceAccount test-sa not to be present in CRB subjects")
|
||||
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
|
||||
secondNs := NewNamespace("")
|
||||
Eventually(func() error {
|
||||
return saClient.Create(context.TODO(), secondNs)
|
||||
}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
|
||||
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(Not(ContainElements(secondNs.GetName())))
|
||||
|
||||
Expect(saClient.Delete(context.TODO(), secondNs)).To(Not(Succeed()))
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
136
e2e/scalability_test.go
Normal file
136
e2e/scalability_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright 2020-2023 Project Capsule Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
)
|
||||
|
||||
var _ = Describe("verify scalability", Label("scalability"), func() {
|
||||
tnt := &capsulev1beta2.Tenant{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-scalability",
|
||||
},
|
||||
Spec: capsulev1beta2.TenantSpec{
|
||||
Owners: capsulev1beta2.OwnerListSpec{
|
||||
{
|
||||
Name: "gatsby",
|
||||
Kind: "User",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
EventuallyCreation(func() error {
|
||||
tnt.ResourceVersion = ""
|
||||
return k8sClient.Create(context.TODO(), tnt)
|
||||
}).Should(Succeed())
|
||||
})
|
||||
JustAfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
|
||||
})
|
||||
|
||||
It("verify lifecycle (scalability)", func() {
|
||||
const amount = 50
|
||||
|
||||
getTenant := func() *capsulev1beta2.Tenant {
|
||||
t := &capsulev1beta2.Tenant{}
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).To(Succeed())
|
||||
return t
|
||||
}
|
||||
|
||||
waitSize := func(expected uint) {
|
||||
Eventually(func() uint {
|
||||
return getTenant().Status.Size
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Equal(expected))
|
||||
}
|
||||
|
||||
waitInstancePresent := func(ns *corev1.Namespace) {
|
||||
Eventually(func() error {
|
||||
t := getTenant()
|
||||
inst := t.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{
|
||||
Name: ns.GetName(),
|
||||
UID: ns.GetUID(),
|
||||
})
|
||||
if inst == nil {
|
||||
return fmt.Errorf("instance not found for ns=%q uid=%q", ns.GetName(), ns.GetUID())
|
||||
}
|
||||
|
||||
condition := inst.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
if inst == nil {
|
||||
return fmt.Errorf("instance not found for ns=%q uid=%q", ns.GetName(), ns.GetUID())
|
||||
}
|
||||
|
||||
if inst.Name != ns.GetName() {
|
||||
return fmt.Errorf("instance.Name=%q, want %q", inst.Name, ns.GetName())
|
||||
}
|
||||
|
||||
cond := inst.Conditions.GetConditionByType(meta.ReadyCondition)
|
||||
if cond == nil {
|
||||
return fmt.Errorf("missing %q condition", meta.ReadyCondition)
|
||||
}
|
||||
if cond.Type != meta.ReadyCondition {
|
||||
return fmt.Errorf("cond.Type=%q, want %q", cond.Type, meta.ReadyCondition)
|
||||
}
|
||||
if cond.Status != metav1.ConditionTrue {
|
||||
return fmt.Errorf("cond.Status=%q, want %q", cond.Status, metav1.ConditionTrue)
|
||||
}
|
||||
if cond.Reason != meta.SucceededReason {
|
||||
return fmt.Errorf("cond.Reason=%q, want %q", cond.Reason, meta.SucceededReason)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
}
|
||||
|
||||
waitInstanceAbsent := func(ns *corev1.Namespace) {
|
||||
Eventually(func() bool {
|
||||
t := getTenant()
|
||||
inst := t.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{
|
||||
Name: ns.GetName(),
|
||||
UID: ns.GetUID(),
|
||||
})
|
||||
return inst == nil
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue())
|
||||
}
|
||||
|
||||
// --- Scale up: create N namespaces and verify Tenant status each time ---
|
||||
namespaces := make([]*corev1.Namespace, 0, amount)
|
||||
for i := 0; i < amount; i++ {
|
||||
ns := NewNamespace(fmt.Sprintf("scale-%d", i))
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
|
||||
|
||||
// Expect size bumped to i+1 and instance present
|
||||
waitSize(uint(i + 1))
|
||||
waitInstancePresent(ns)
|
||||
|
||||
namespaces = append(namespaces, ns)
|
||||
}
|
||||
|
||||
// --- Scale down: delete N namespaces and verify Tenant status each time ---
|
||||
for i := 0; i < amount; i++ {
|
||||
ns := namespaces[i]
|
||||
Expect(k8sClient.Delete(context.TODO(), ns)).To(Succeed())
|
||||
|
||||
// Expect size decremented and instance absent
|
||||
waitSize(uint(amount - i - 1))
|
||||
waitInstanceAbsent(ns)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
@@ -73,3 +74,19 @@ func ownerClient(owner capsulev1beta2.OwnerSpec) (cs kubernetes.Interface) {
|
||||
|
||||
return cs
|
||||
}
|
||||
|
||||
func impersonationClient(user string, groups []string, scheme *runtime.Scheme) client.Client {
|
||||
c, err := config.GetConfig()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
c.Impersonate = rest.ImpersonationConfig{
|
||||
UserName: user,
|
||||
Groups: groups,
|
||||
}
|
||||
cl, err := client.New(c, client.Options{Scheme: scheme})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
return cl
|
||||
}
|
||||
|
||||
func withDefaultGroups(groups []string) []string {
|
||||
return append([]string{"projectcapsule.dev"}, groups...)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/utils"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
@@ -60,6 +60,19 @@ var _ = Describe("cordoning a Tenant", Label("tenant"), func() {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
By("Verifing Tenant Status", func() {
|
||||
t := &capsulev1beta2.Tenant{}
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
|
||||
|
||||
condition := t.Status.Conditions.GetConditionByType(meta.CordonedCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionFalse), "Expected tenant condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.CordonedCondition), "Expected tenant condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.ActiveReason), "Expected tenant condition reason to be Succeeded")
|
||||
})
|
||||
|
||||
By("creating a Namespace", func() {
|
||||
NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed())
|
||||
|
||||
@@ -79,10 +92,22 @@ var _ = Describe("cordoning a Tenant", Label("tenant"), func() {
|
||||
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
|
||||
|
||||
Expect(ns.Labels).To(HaveKey(utils.CordonedLabel))
|
||||
Expect(ns.Labels).To(HaveKey(meta.CordonedLabel))
|
||||
|
||||
})
|
||||
|
||||
By("Verifing Tenant Status", func() {
|
||||
t := &capsulev1beta2.Tenant{}
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
|
||||
|
||||
condition := t.Status.Conditions.GetConditionByType(meta.CordonedCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionTrue), "Expected tenant condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.CordonedCondition), "Expected tenant condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.CordonedReason), "Expected tenant condition reason to be Succeeded")
|
||||
})
|
||||
|
||||
By("cordoning the Tenant deletion must be blocked", func() {
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.Name}, tnt)).Should(Succeed())
|
||||
|
||||
@@ -116,8 +141,20 @@ var _ = Describe("cordoning a Tenant", Label("tenant"), func() {
|
||||
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed())
|
||||
|
||||
Expect(ns.Labels).ToNot(HaveKey(utils.CordonedLabel))
|
||||
Expect(ns.Labels).ToNot(HaveKey(meta.CordonedLabel))
|
||||
|
||||
})
|
||||
|
||||
By("Verifing Tenant Status", func() {
|
||||
t := &capsulev1beta2.Tenant{}
|
||||
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
|
||||
|
||||
condition := t.Status.Conditions.GetConditionByType(meta.CordonedCondition)
|
||||
Expect(condition).NotTo(BeNil(), "Condition instance should not be nil")
|
||||
|
||||
Expect(condition.Status).To(Equal(metav1.ConditionFalse), "Expected tenant condition status to be True")
|
||||
Expect(condition.Type).To(Equal(meta.CordonedCondition), "Expected tenant condition type to be Ready")
|
||||
Expect(condition.Reason).To(Equal(meta.ActiveReason), "Expected tenant condition reason to be Succeeded")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
48
go.mod
48
go.mod
@@ -2,28 +2,29 @@ module github.com/projectcapsule/capsule
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.6
|
||||
toolchain go1.25.3
|
||||
|
||||
require (
|
||||
github.com/go-logr/logr v1.4.3
|
||||
github.com/onsi/ginkgo/v2 v2.25.1
|
||||
github.com/onsi/ginkgo/v2 v2.27.1
|
||||
github.com/onsi/gomega v1.38.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.23.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/valyala/fasttemplate v1.2.2
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/sync v0.16.0
|
||||
k8s.io/api v0.34.0
|
||||
k8s.io/apiextensions-apiserver v0.34.0
|
||||
k8s.io/apimachinery v0.34.0
|
||||
k8s.io/client-go v0.34.0
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
|
||||
sigs.k8s.io/cluster-api v1.11.1
|
||||
sigs.k8s.io/controller-runtime v0.22.0
|
||||
sigs.k8s.io/gateway-api v1.3.0
|
||||
golang.org/x/sync v0.17.0
|
||||
k8s.io/api v0.34.1
|
||||
k8s.io/apiextensions-apiserver v0.34.1
|
||||
k8s.io/apimachinery v0.34.1
|
||||
k8s.io/apiserver v0.34.1
|
||||
k8s.io/client-go v0.34.1
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
|
||||
sigs.k8s.io/cluster-api v1.11.2
|
||||
sigs.k8s.io/controller-runtime v0.22.3
|
||||
sigs.k8s.io/gateway-api v1.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -59,7 +60,6 @@ require (
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
@@ -68,29 +68,29 @@ require (
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.0 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/term v0.34.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/oauth2 v0.31.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250902184714-7fc278399c7f // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
|
||||
74
go.sum
74
go.sum
@@ -98,8 +98,6 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
@@ -134,8 +132,12 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo/v2 v2.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY=
|
||||
github.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk=
|
||||
github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw=
|
||||
github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE=
|
||||
github.com/onsi/ginkgo/v2 v2.26.0 h1:1J4Wut1IlYZNEAWIV3ALrT9NfiaGW2cDCJQSFQMs/gE=
|
||||
github.com/onsi/ginkgo/v2 v2.26.0/go.mod h1:qhEywmzWTBUY88kfO0BRvX4py7scov9yR+Az2oavUzw=
|
||||
github.com/onsi/ginkgo/v2 v2.27.1 h1:0LJC8MpUSQnfnp4n/3W3GdlmJP3ENGF0ZPzjQGLPP7s=
|
||||
github.com/onsi/ginkgo/v2 v2.27.1/go.mod h1:wmy3vCqiBjirARfVhAqFpYt8uvX0yaFe+GudAqqcCqA=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
@@ -147,14 +149,12 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/common v0.66.0 h1:K/rJPHrG3+AoQs50r2+0t7zMnMzek2Vbv31OFVsMeVY=
|
||||
github.com/prometheus/common v0.66.0/go.mod h1:Ux6NtV1B4LatamKE63tJBntoxD++xmtI/lK0VtEplN4=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
@@ -216,6 +216,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -223,36 +225,53 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
|
||||
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
@@ -273,6 +292,8 @@ google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@@ -280,43 +301,60 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE=
|
||||
k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug=
|
||||
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
|
||||
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
|
||||
k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc=
|
||||
k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0=
|
||||
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
|
||||
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
|
||||
k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0=
|
||||
k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
|
||||
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apiserver v0.34.0 h1:Z51fw1iGMqN7uJ1kEaynf2Aec1Y774PqU+FVWCFV3Jg=
|
||||
k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ=
|
||||
k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
|
||||
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
|
||||
k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo=
|
||||
k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY=
|
||||
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
|
||||
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
|
||||
k8s.io/cluster-bootstrap v0.33.3 h1:u2NTxJ5CFSBFXaDxLQoOWMly8eni31psVso+caq6uwI=
|
||||
k8s.io/cluster-bootstrap v0.33.3/go.mod h1:p970f8u8jf273zyQ5raD8WUu2XyAl0SAWOY82o7i/ds=
|
||||
k8s.io/component-base v0.34.0 h1:bS8Ua3zlJzapklsB1dZgjEJuJEeHjj8yTu1gxE2zQX8=
|
||||
k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg=
|
||||
k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 h1:liMHz39T5dJO1aOKHLvwaCjDbf07wVh6yaUlTpunnkE=
|
||||
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/kube-openapi v0.0.0-20250902184714-7fc278399c7f h1:wyRlmLgBSXi3kgawro8klrMRljXeRo1HFkQRs+meYfs=
|
||||
k8s.io/kube-openapi v0.0.0-20250902184714-7fc278399c7f/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0=
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||
sigs.k8s.io/cluster-api v1.11.0 h1:4ZqKxjhdP3F/vvHMd675rGsDrT/siggnFPt5eKQ8nkI=
|
||||
sigs.k8s.io/cluster-api v1.11.0/go.mod h1:gGmNlHrtJe3z0YV3J6JRy5Rwh9SfzokjQaS+Fv3DBPE=
|
||||
sigs.k8s.io/cluster-api v1.11.1 h1:7CyGCTxv1p3Y2kRe1ljTj/w4TcdIdWNj0CTBc4i1aBo=
|
||||
sigs.k8s.io/cluster-api v1.11.1/go.mod h1:zyrjgJ5RbXhwKcAdUlGPNK5YOHpcmxXvur+5I8lkMUQ=
|
||||
sigs.k8s.io/controller-runtime v0.22.0 h1:mTOfibb8Hxwpx3xEkR56i7xSjB+nH4hZG37SrlCY5e0=
|
||||
sigs.k8s.io/controller-runtime v0.22.0/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY=
|
||||
sigs.k8s.io/cluster-api v1.11.2 h1:uAczaBavU5Y6aDgyoXWtq28k1kalpSZnVItwXHusw1c=
|
||||
sigs.k8s.io/cluster-api v1.11.2/go.mod h1:C1gJVAjMXRG+M+djjGYNkoi5kBMhFnOUI9QqZDAtMms=
|
||||
sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg=
|
||||
sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY=
|
||||
sigs.k8s.io/controller-runtime v0.22.2 h1:cK2l8BGWsSWkXz09tcS4rJh95iOLney5eawcK5A33r4=
|
||||
sigs.k8s.io/controller-runtime v0.22.2/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y=
|
||||
sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
sigs.k8s.io/gateway-api v1.3.0 h1:q6okN+/UKDATola4JY7zXzx40WO4VISk7i9DIfOvr9M=
|
||||
sigs.k8s.io/gateway-api v1.3.0/go.mod h1:d8NV8nJbaRbEKem+5IuxkL8gJGOZ+FJ+NvOIltV8gDk=
|
||||
sigs.k8s.io/gateway-api v1.4.0 h1:ZwlNM6zOHq0h3WUX2gfByPs2yAEsy/EenYJB78jpQfQ=
|
||||
sigs.k8s.io/gateway-api v1.4.0/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
|
||||
@@ -16,6 +16,7 @@ type AdditionalMetadataSpec struct {
|
||||
|
||||
type AdditionalMetadataSelectorSpec struct {
|
||||
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
@@ -11,4 +11,8 @@ type AdditionalRoleBindingsSpec struct {
|
||||
ClusterRoleName string `json:"clusterRoleName"`
|
||||
// kubebuilder:validation:Minimum=1
|
||||
Subjects []rbacv1.Subject `json:"subjects"`
|
||||
// Additional Labels for the synchronized rolebindings
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
// Additional Annotations for the synchronized rolebindings
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func (in *AllowedListSpec) ExactMatch(value string) (ok bool) {
|
||||
ok = i < len(in.Exact) && in.Exact[i] == value
|
||||
}
|
||||
|
||||
return
|
||||
return ok
|
||||
}
|
||||
|
||||
func (in *AllowedListSpec) RegexMatch(value string) (ok bool) {
|
||||
@@ -87,7 +87,7 @@ func (in *AllowedListSpec) RegexMatch(value string) (ok bool) {
|
||||
ok = regexp.MustCompile(in.Regex).MatchString(value)
|
||||
}
|
||||
|
||||
return
|
||||
return ok
|
||||
}
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
|
||||
@@ -35,7 +35,7 @@ func (in ForbiddenListSpec) ExactMatch(value string) (ok bool) {
|
||||
ok = i < len(in.Exact) && in.Exact[i] == value
|
||||
}
|
||||
|
||||
return
|
||||
return ok
|
||||
}
|
||||
|
||||
func (in ForbiddenListSpec) RegexMatch(value string) (ok bool) {
|
||||
@@ -43,7 +43,7 @@ func (in ForbiddenListSpec) RegexMatch(value string) (ok bool) {
|
||||
ok = regexp.MustCompile(in.Regex).MatchString(value)
|
||||
}
|
||||
|
||||
return
|
||||
return ok
|
||||
}
|
||||
|
||||
type ForbiddenError struct {
|
||||
@@ -76,7 +76,7 @@ func (f *ForbiddenError) appendForbiddenError() (append string) {
|
||||
append += fmt.Sprintf("matching the regex %s", f.spec.Regex)
|
||||
}
|
||||
|
||||
return
|
||||
return append
|
||||
}
|
||||
|
||||
func ValidateForbidden(metadata map[string]string, forbiddenList ForbiddenListSpec) error {
|
||||
|
||||
@@ -85,6 +85,20 @@ func (in *AdditionalRoleBindingsSpec) DeepCopyInto(out *AdditionalRoleBindingsSp
|
||||
*out = make([]rbacv1.Subject, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Annotations != nil {
|
||||
in, out := &in.Annotations, &out.Annotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalRoleBindingsSpec.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user