mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-02-22 13:54:06 +00:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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@17783bfb99b07f70fae080b654aed0c514057477
|
||||
with:
|
||||
sarif_file: gosec.sarif
|
||||
unit_tests:
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
value: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Upload Report to Codecov
|
||||
if: ${{ steps.checksecret.outputs.result == 'true' }}
|
||||
uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: projectcapsule/capsule
|
||||
|
||||
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@17783bfb99b07f70fae080b654aed0c514057477
|
||||
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@d8a2c0130026bf585de5c176ab8f7ce62d75bf04
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||
uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
||||
with:
|
||||
|
||||
4
.github/workflows/scorecard.yml
vendored
4
.github/workflows/scorecard.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run analysis
|
||||
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
|
||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@@ -37,6 +37,6 @@ jobs:
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
- name: Upload to code-scanning
|
||||
uses: github/codeql-action/upload-sarif@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0
|
||||
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
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@65d1d4804d3060875fff9f9fa8a49e27f71ce7f0
|
||||
with:
|
||||
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 30 days.'
|
||||
stale-pr-message: 'This pull request has been marked as stale because it has been inactive for more than 30 days. Please update this pull request or it will be automatically closed in 30 days.'
|
||||
|
||||
@@ -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" />
|
||||
|
||||
10
Makefile
10
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) || \
|
||||
@@ -383,7 +385,7 @@ nwa:
|
||||
$(call go-install-tool,$(NWA),github.com/$(NWA_LOOKUP)@$(NWA_VERSION))
|
||||
|
||||
GOLANGCI_LINT := $(LOCALBIN)/golangci-lint
|
||||
GOLANGCI_LINT_VERSION := v2.4.0
|
||||
GOLANGCI_LINT_VERSION := v2.5.0
|
||||
GOLANGCI_LINT_LOOKUP := golangci/golangci-lint
|
||||
golangci-lint: ## Download golangci-lint locally if necessary.
|
||||
@test -s $(GOLANGCI_LINT) && $(GOLANGCI_LINT) -h | grep -q $(GOLANGCI_LINT_VERSION) || \
|
||||
|
||||
@@ -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 |
@@ -26,6 +26,7 @@ The following Values have changed key or Value:
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| crds.annnotations | object | `{}` | Extra Annotations for CRDs |
|
||||
| crds.createConfig | bool | `false` | Create additionally CapsuleConfiguration even if CRDs are exclusive |
|
||||
| crds.exclusive | bool | `false` | Only install the CRDs, no other primitives |
|
||||
| crds.install | bool | `true` | Install the CustomResourceDefinitions (This also manages the lifecycle of the CRDs for update operations) |
|
||||
| crds.labels | object | `{}` | Extra Labels for CRDs |
|
||||
@@ -105,11 +106,14 @@ The following Values have changed key or Value:
|
||||
| manager.image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. |
|
||||
| manager.kind | string | `"Deployment"` | Set the controller deployment mode as `Deployment` or `DaemonSet`. |
|
||||
| manager.livenessProbe | object | `{"httpGet":{"path":"/healthz","port":10080}}` | Configure the liveness probe using Deployment probe spec |
|
||||
| manager.options.allowServiceAccountPromotion | bool | `false` | ServiceAccounts within tenant namespaces can be promoted to owners of the given tenant this can be achieved by labeling the serviceaccount and then they are considered owners. This can only be done by other owners of the tenant. However ServiceAccounts which have been promoted to owner can not promote further serviceAccounts. |
|
||||
| manager.options.annotations | object | `{}` | Additional annotations to add to the CapsuleConfiguration resource |
|
||||
| manager.options.capsuleConfiguration | string | `"default"` | Change the default name of the capsule configuration name |
|
||||
| manager.options.capsuleUserGroups | list | `["projectcapsule.dev"]` | Names of the groups considered as Capsule users. |
|
||||
| manager.options.forceTenantPrefix | bool | `false` | Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash |
|
||||
| manager.options.generateCertificates | bool | `true` | Specifies whether capsule webhooks certificates should be generated by capsule operator |
|
||||
| manager.options.ignoreUserWithGroups | list | `[]` | Define groups which when found in the request of a user will be ignored by the Capsule this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups. |
|
||||
| manager.options.labels | object | `{}` | Additional labels to add to the CapsuleConfiguration resource |
|
||||
| manager.options.logLevel | string | `"4"` | Set the log verbosity of the capsule with a value from 1 to 10 |
|
||||
| manager.options.nodeMetadata | object | `{"forbiddenAnnotations":{"denied":[],"deniedRegex":""},"forbiddenLabels":{"denied":[],"deniedRegex":""}}` | Allows to set the forbidden metadata for the worker nodes that could be patched by a Tenant |
|
||||
| manager.options.protectedNamespaceRegex | string | `""` | If specified, disallows creation of namespaces matching the passed regexp |
|
||||
@@ -133,7 +137,7 @@ The following Values have changed key or Value:
|
||||
| monitoring.dashboards.labels | object | `{}` | Labels for dashboard configmaps |
|
||||
| monitoring.dashboards.namespace | string | `""` | Custom namespace for dashboard configmaps |
|
||||
| monitoring.dashboards.operator.allowCrossNamespaceImport | bool | `true` | Allow the Operator to match this resource with Grafanas outside the current namespace |
|
||||
| monitoring.dashboards.operator.enabled | bool | `true` | Enable Operator Resources (GrafanaDashboard) |
|
||||
| monitoring.dashboards.operator.enabled | bool | `false` | Enable Operator Resources (GrafanaDashboard) |
|
||||
| monitoring.dashboards.operator.folder | string | `""` | folder assignment for dashboard |
|
||||
| monitoring.dashboards.operator.instanceSelector | object | `{}` | Selects Grafana instances for import |
|
||||
| monitoring.dashboards.operator.resyncPeriod | string | `"10m"` | How often the resource is synced, defaults to 10m0s if not set |
|
||||
@@ -196,7 +200,7 @@ The following Values have changed key or Value:
|
||||
| webhooks.hooks.networkpolicies.matchPolicy | string | `"Equivalent"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
| webhooks.hooks.networkpolicies.namespaceSelector | object | `{"matchExpressions":[{"key":"capsule.clastix.io/tenant","operator":"Exists"}]}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
|
||||
| webhooks.hooks.networkpolicies.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
|
||||
| webhooks.hooks.nodes.enabled | bool | `true` | Enable the Hook |
|
||||
| webhooks.hooks.nodes.enabled | bool | `false` | Enable the Hook |
|
||||
| webhooks.hooks.nodes.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
|
||||
| webhooks.hooks.nodes.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
| webhooks.hooks.nodes.matchPolicy | string | `"Exact"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
@@ -228,6 +232,12 @@ The following Values have changed key or Value:
|
||||
| webhooks.hooks.resourcepools.pools.matchPolicy | string | `"Equivalent"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
| webhooks.hooks.resourcepools.pools.namespaceSelector | object | `{}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
|
||||
| webhooks.hooks.resourcepools.pools.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
|
||||
| webhooks.hooks.serviceaccounts.enabled | bool | `true` | Enable the Hook |
|
||||
| webhooks.hooks.serviceaccounts.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
|
||||
| webhooks.hooks.serviceaccounts.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
| webhooks.hooks.serviceaccounts.matchPolicy | string | `"Exact"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
| webhooks.hooks.serviceaccounts.namespaceSelector | object | `{"matchExpressions":[{"key":"capsule.clastix.io/tenant","operator":"Exists"}]}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
|
||||
| webhooks.hooks.serviceaccounts.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
|
||||
| webhooks.hooks.services.enabled | bool | `true` | Enable the Hook |
|
||||
| webhooks.hooks.services.failurePolicy | string | `"Fail"` | [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy) |
|
||||
| webhooks.hooks.services.matchConditions | list | `[]` | [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
@@ -246,6 +256,7 @@ The following Values have changed key or Value:
|
||||
| webhooks.hooks.tenants.matchPolicy | string | `"Exact"` | [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy) |
|
||||
| webhooks.hooks.tenants.namespaceSelector | object | `{}` | [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) |
|
||||
| webhooks.hooks.tenants.objectSelector | object | `{}` | [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) |
|
||||
| webhooks.hooks.tenants.reinvocationPolicy | string | `"Never"` | [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy) |
|
||||
| webhooks.mutatingWebhooksTimeoutSeconds | int | `30` | Timeout in seconds for mutating webhooks |
|
||||
| webhooks.service.caBundle | string | `""` | CABundle for the webhook service |
|
||||
| webhooks.service.name | string | `""` | Custom service name for the webhook service |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,9 @@ spec:
|
||||
{{- if .Values.podSecurityContext.enabled }}
|
||||
securityContext: {{- omit .Values.podSecurityContext "enabled" | toYaml | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if not .Values.manager.hostUsers }}
|
||||
hostUsers: {{ .Values.manager.hostUsers }}
|
||||
{{- end }}
|
||||
{{- if .Values.manager.hostNetwork }}
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
{{- if not $.Values.crds.exclusive }}
|
||||
{{- if or (not $.Values.crds.exclusive) ($.Values.crds.createConfig) }}
|
||||
apiVersion: capsule.clastix.io/v1beta2
|
||||
kind: CapsuleConfiguration
|
||||
metadata:
|
||||
name: default
|
||||
labels:
|
||||
{{- include "capsule.labels" . | nindent 4 }}
|
||||
{{- with .Values.manager.options.labels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
annotations:
|
||||
{{- with .Values.customAnnotations }}
|
||||
{{- with (mergeOverwrite .Values.customAnnotations .Values.manager.options.annotations) }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
@@ -16,6 +19,7 @@ spec:
|
||||
TLSSecretName: {{ include "capsule.secretTlsName" . }}
|
||||
validatingWebhookConfigurationName: {{ include "capsule.fullname" . }}-validating-webhook-configuration
|
||||
forceTenantPrefix: {{ .Values.manager.options.forceTenantPrefix }}
|
||||
allowServiceAccountPromotion: {{ .Values.manager.options.allowServiceAccountPromotion }}
|
||||
userGroups:
|
||||
{{- toYaml .Values.manager.options.capsuleUserGroups | nindent 4 }}
|
||||
userNames:
|
||||
|
||||
@@ -23,11 +23,19 @@ rules:
|
||||
- apiextensions.k8s.io
|
||||
resources:
|
||||
- customresourcedefinitions
|
||||
resourceNames:
|
||||
- capsuleconfigurations.capsule.clastix.io
|
||||
- resourcepoolclaims.capsule.clastix.io
|
||||
- resourcepools.capsule.clastix.io
|
||||
- tenantresources.capsule.clastix.io
|
||||
- globaltenantresources.capsule.clastix.io
|
||||
- tenants.capsule.clastix.io
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -261,6 +261,7 @@ webhooks:
|
||||
- UPDATE
|
||||
resources:
|
||||
- pods
|
||||
- pods/ephemeralcontainers
|
||||
scope: Namespaced
|
||||
sideEffects: None
|
||||
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
|
||||
@@ -382,7 +383,7 @@ webhooks:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- include "capsule.webhooks.service" (dict "path" "/tenants" "ctx" $) | nindent 4 }}
|
||||
{{- include "capsule.webhooks.service" (dict "path" "/tenants/validating" "ctx" $) | nindent 4 }}
|
||||
failurePolicy: {{ .failurePolicy }}
|
||||
matchPolicy: {{ .matchPolicy }}
|
||||
{{- with .namespaceSelector }}
|
||||
@@ -408,7 +409,7 @@ webhooks:
|
||||
- DELETE
|
||||
resources:
|
||||
- tenants
|
||||
scope: '*'
|
||||
scope: 'Cluster'
|
||||
sideEffects: None
|
||||
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
|
||||
{{- end }}
|
||||
@@ -480,6 +481,7 @@ webhooks:
|
||||
operations:
|
||||
- CREATE
|
||||
- UPDATE
|
||||
- DELETE
|
||||
resources:
|
||||
- resourcepoolclaims
|
||||
scope: '*'
|
||||
@@ -525,4 +527,41 @@ webhooks:
|
||||
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with .Values.webhooks.hooks.serviceaccounts }}
|
||||
{{- if .enabled }}
|
||||
- name: serviceaccounts.tenant.projectcapsule.dev
|
||||
admissionReviewVersions:
|
||||
- v1
|
||||
- v1beta1
|
||||
clientConfig:
|
||||
{{- include "capsule.webhooks.service" (dict "path" "/serviceaccounts" "ctx" $) | nindent 4 }}
|
||||
failurePolicy: {{ .failurePolicy }}
|
||||
matchPolicy: {{ .matchPolicy }}
|
||||
{{- with .namespaceSelector }}
|
||||
namespaceSelector:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .objectSelector }}
|
||||
objectSelector:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .matchConditions }}
|
||||
matchConditions:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- '*'
|
||||
apiVersions:
|
||||
- '*'
|
||||
operations:
|
||||
- CREATE
|
||||
- UPDATE
|
||||
resources:
|
||||
- 'serviceaccounts'
|
||||
scope: Namespaced
|
||||
sideEffects: None
|
||||
timeoutSeconds: {{ $.Values.webhooks.validatingWebhooksTimeoutSeconds }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
"description": "Extra Annotations for CRDs",
|
||||
"type": "object"
|
||||
},
|
||||
"createConfig": {
|
||||
"description": "Create additionally CapsuleConfiguration even if CRDs are exclusive",
|
||||
"type": "boolean"
|
||||
},
|
||||
"exclusive": {
|
||||
"description": "Only install the CRDs, no other primitives",
|
||||
"type": "boolean"
|
||||
@@ -289,6 +293,14 @@
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allowServiceAccountPromotion": {
|
||||
"description": "ServiceAccounts within tenant namespaces can be promoted to owners of the given tenant this can be achieved by labeling the serviceaccount and then they are considered owners. This can only be done by other owners of the tenant. However ServiceAccounts which have been promoted to owner can not promote further serviceAccounts.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"annotations": {
|
||||
"description": "Additional annotations to add to the CapsuleConfiguration resource",
|
||||
"type": "object"
|
||||
},
|
||||
"capsuleConfiguration": {
|
||||
"description": "Change the default name of the capsule configuration name",
|
||||
"type": "string"
|
||||
@@ -312,6 +324,10 @@
|
||||
"description": "Define groups which when found in the request of a user will be ignored by the Capsule this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups.",
|
||||
"type": "array"
|
||||
},
|
||||
"labels": {
|
||||
"description": "Additional labels to add to the CapsuleConfiguration resource",
|
||||
"type": "object"
|
||||
},
|
||||
"logLevel": {
|
||||
"description": "Set the log verbosity of the capsule with a value from 1 to 10",
|
||||
"type": "string"
|
||||
@@ -1194,6 +1210,51 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"serviceaccounts": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Enable the Hook",
|
||||
"type": "boolean"
|
||||
},
|
||||
"failurePolicy": {
|
||||
"description": "[FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy)",
|
||||
"type": "string"
|
||||
},
|
||||
"matchConditions": {
|
||||
"description": "[MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)",
|
||||
"type": "array"
|
||||
},
|
||||
"matchPolicy": {
|
||||
"description": "[MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)",
|
||||
"type": "string"
|
||||
},
|
||||
"namespaceSelector": {
|
||||
"description": "[NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"matchExpressions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"operator": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"objectSelector": {
|
||||
"description": "[ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector)",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1326,6 +1387,10 @@
|
||||
"objectSelector": {
|
||||
"description": "[ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector)",
|
||||
"type": "object"
|
||||
},
|
||||
"reinvocationPolicy": {
|
||||
"description": "[ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@ crds:
|
||||
install: true
|
||||
# -- Only install the CRDs, no other primitives
|
||||
exclusive: false
|
||||
# -- Create additionally CapsuleConfiguration even if CRDs are exclusive
|
||||
createConfig: false
|
||||
# -- Extra Labels for CRDs
|
||||
labels: {}
|
||||
# -- Extra Annotations for CRDs
|
||||
@@ -156,12 +158,14 @@ manager:
|
||||
|
||||
# Additional Capsule Controller Options
|
||||
options:
|
||||
# -- Additional labels to add to the CapsuleConfiguration resource
|
||||
labels: {}
|
||||
# -- Additional annotations to add to the CapsuleConfiguration resource
|
||||
annotations: {}
|
||||
# -- Change the default name of the capsule configuration name
|
||||
capsuleConfiguration: default
|
||||
# -- Set the log verbosity of the capsule with a value from 1 to 10
|
||||
logLevel: '4'
|
||||
# -- Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash
|
||||
forceTenantPrefix: false
|
||||
# -- Names of the users considered as Capsule users.
|
||||
userNames: []
|
||||
# -- Names of the groups considered as Capsule users.
|
||||
@@ -169,6 +173,12 @@ manager:
|
||||
# -- Define groups which when found in the request of a user will be ignored by the Capsule
|
||||
# this might be useful if you have one group where all the users are in, but you want to separate administrators from normal users with additional groups.
|
||||
ignoreUserWithGroups: []
|
||||
# -- ServiceAccounts within tenant namespaces can be promoted to owners of the given tenant
|
||||
# this can be achieved by labeling the serviceaccount and then they are considered owners. This can only be done by other owners of the tenant.
|
||||
# However ServiceAccounts which have been promoted to owner can not promote further serviceAccounts.
|
||||
allowServiceAccountPromotion: false
|
||||
# -- Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash
|
||||
forceTenantPrefix: false
|
||||
# -- If specified, disallows creation of namespaces matching the passed regexp
|
||||
protectedNamespaceRegex: ""
|
||||
# -- Specifies whether capsule webhooks certificates should be generated by capsule operator
|
||||
@@ -218,9 +228,6 @@ imagePullSecrets: []
|
||||
|
||||
# -- Labels to add to the capsule pod.
|
||||
podLabels: {}
|
||||
# The following annotations guarantee scheduling for critical add-on pods
|
||||
# podAnnotations:
|
||||
# scheduler.alpha.kubernetes.io/critical-pod: ''
|
||||
|
||||
# -- Annotations to add to the capsule pod.
|
||||
podAnnotations: {}
|
||||
@@ -311,7 +318,7 @@ monitoring:
|
||||
# Grafana Operator
|
||||
operator:
|
||||
# -- Enable Operator Resources (GrafanaDashboard)
|
||||
enabled: true
|
||||
enabled: false
|
||||
# -- Allow the Operator to match this resource with Grafanas outside the current namespace
|
||||
allowCrossNamespaceImport: true
|
||||
# -- How often the resource is synced, defaults to 10m0s if not set
|
||||
@@ -569,6 +576,8 @@ webhooks:
|
||||
namespaceSelector: {}
|
||||
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
|
||||
matchConditions: []
|
||||
# -- [ReinvocationPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#reinvocation-policy)
|
||||
reinvocationPolicy: Never
|
||||
|
||||
tenantResourceObjects:
|
||||
# -- Enable the Hook
|
||||
@@ -609,7 +618,7 @@ webhooks:
|
||||
|
||||
nodes:
|
||||
# -- Enable the Hook
|
||||
enabled: true
|
||||
enabled: false
|
||||
# -- [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy)
|
||||
failurePolicy: Fail
|
||||
# -- [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
|
||||
@@ -621,6 +630,23 @@ webhooks:
|
||||
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
|
||||
matchConditions: []
|
||||
|
||||
serviceaccounts:
|
||||
# -- Enable the Hook
|
||||
enabled: true
|
||||
# -- [FailurePolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy)
|
||||
failurePolicy: Fail
|
||||
# -- [MatchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
|
||||
matchPolicy: Exact
|
||||
# -- [ObjectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector)
|
||||
objectSelector: {}
|
||||
# -- [NamespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector)
|
||||
namespaceSelector:
|
||||
matchExpressions:
|
||||
- key: capsule.clastix.io/tenant
|
||||
operator: Exists
|
||||
# -- [MatchConditions](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy)
|
||||
matchConditions: []
|
||||
|
||||
# -- Deprecated, use webhooks.hooks.namespaces instead
|
||||
namespaceOwnerReference: {}
|
||||
|
||||
|
||||
16
cmd/main.go
16
cmd/main.go
@@ -56,7 +56,9 @@ import (
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/resourcepool"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/route"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/service"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/tenant"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/serviceaccounts"
|
||||
tenantmutation "github.com/projectcapsule/capsule/pkg/webhook/tenant/mutation"
|
||||
tenantvalidation "github.com/projectcapsule/capsule/pkg/webhook/tenant/validation"
|
||||
tntresource "github.com/projectcapsule/capsule/pkg/webhook/tenantresource"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
)
|
||||
@@ -226,18 +228,20 @@ func main() {
|
||||
// webhooks: the order matters, don't change it and just append
|
||||
webhooksList := append(
|
||||
make([]webhook.Webhook, 0),
|
||||
route.Pod(pod.ImagePullPolicy(), pod.ContainerRegistry(), pod.PriorityClass(), pod.RuntimeClass()),
|
||||
route.Namespace(utils.InCapsuleGroups(cfg, namespacevalidation.PatchHandler(), namespacevalidation.QuotaHandler(), namespacevalidation.FreezeHandler(cfg), namespacevalidation.PrefixHandler(cfg), namespacevalidation.UserMetadataHandler())),
|
||||
route.Pod(pod.ImagePullPolicy(), pod.ContainerRegistry(cfg), pod.PriorityClass(), pod.RuntimeClass()),
|
||||
route.Namespace(utils.InCapsuleGroups(cfg, namespacevalidation.PatchHandler(cfg), namespacevalidation.QuotaHandler(), namespacevalidation.FreezeHandler(cfg), namespacevalidation.PrefixHandler(cfg), namespacevalidation.UserMetadataHandler())),
|
||||
route.Ingress(ingress.Class(cfg, kubeVersion), ingress.Hostnames(cfg), ingress.Collision(cfg), ingress.Wildcard()),
|
||||
route.PVC(pvc.Validating(), pvc.PersistentVolumeReuse()),
|
||||
route.Service(service.Handler()),
|
||||
route.TenantResourceObjects(utils.InCapsuleGroups(cfg, tntresource.WriteOpsHandler())),
|
||||
route.NetworkPolicy(utils.InCapsuleGroups(cfg, networkpolicy.Handler())),
|
||||
route.Tenant(tenant.NameHandler(), tenant.RoleBindingRegexHandler(), tenant.IngressClassRegexHandler(), tenant.StorageClassRegexHandler(), tenant.ContainerRegistryRegexHandler(), tenant.HostnameRegexHandler(), tenant.FreezedEmitter(), tenant.ServiceAccountNameHandler(), tenant.ForbiddenAnnotationsRegexHandler(), tenant.ProtectedHandler(), tenant.MetaHandler()),
|
||||
route.Cordoning(tenant.CordoningHandler(cfg)),
|
||||
route.TenantMutating(tenantmutation.MetaHandler()),
|
||||
route.TenantValidating(tenantvalidation.NameHandler(), tenantvalidation.RoleBindingRegexHandler(), tenantvalidation.IngressClassRegexHandler(), tenantvalidation.StorageClassRegexHandler(), tenantvalidation.ContainerRegistryRegexHandler(), tenantvalidation.HostnameRegexHandler(), tenantvalidation.FreezedEmitter(), tenantvalidation.ServiceAccountNameHandler(), tenantvalidation.ForbiddenAnnotationsRegexHandler(), tenantvalidation.ProtectedHandler()),
|
||||
route.Cordoning(tenantvalidation.CordoningHandler(cfg)),
|
||||
route.Node(utils.InCapsuleGroups(cfg, node.UserMetadataHandler(cfg, kubeVersion))),
|
||||
route.ServiceAccounts(serviceaccounts.Handler(cfg)),
|
||||
route.NamespacePatch(utils.InCapsuleGroups(cfg, namespacemutation.CordoningLabelHandler(cfg), namespacemutation.OwnerReferenceHandler(cfg), namespacemutation.MetadataHandler(cfg))),
|
||||
route.CustomResources(tenant.ResourceCounterHandler(manager.GetClient())),
|
||||
route.CustomResources(tenantvalidation.ResourceCounterHandler(manager.GetClient())),
|
||||
route.Gateway(gateway.Class(cfg)),
|
||||
route.Defaults(defaults.Handler(cfg, kubeVersion)),
|
||||
route.ResourcePoolMutation((resourcepool.PoolMutationHandler(ctrl.Log.WithName("webhooks").WithName("resourcepool")))),
|
||||
|
||||
@@ -42,5 +42,5 @@ func (c *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
|
||||
c.Log.Info("CapsuleConfiguration reconciliation finished", "request.name", request.Name)
|
||||
|
||||
return
|
||||
return res, err
|
||||
}
|
||||
|
||||
@@ -9,9 +9,11 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -23,6 +25,7 @@ import (
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/controllers/utils"
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
@@ -47,17 +50,31 @@ func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, config
|
||||
Watches(&capsulev1beta2.CapsuleConfiguration{}, handler.Funcs{
|
||||
UpdateFunc: func(ctx context.Context, updateEvent event.TypedUpdateEvent[client.Object], limitingInterface workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
if updateEvent.ObjectNew.GetName() == configurationName {
|
||||
if crbErr := r.EnsureClusterRoleBindings(ctx); crbErr != nil {
|
||||
if crbErr := r.EnsureClusterRoleBindingsProvisioner(ctx); crbErr != nil {
|
||||
r.Log.Error(err, "cannot update ClusterRoleBinding upon CapsuleConfiguration update")
|
||||
}
|
||||
}
|
||||
},
|
||||
}).Complete(r)
|
||||
}).
|
||||
Watches(&corev1.ServiceAccount{}, handler.Funcs{
|
||||
CreateFunc: func(ctx context.Context, e event.TypedCreateEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
r.handleSAChange(ctx, e.Object)
|
||||
},
|
||||
UpdateFunc: func(ctx context.Context, e event.TypedUpdateEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
if promotionLabelsChanged(e.ObjectOld.GetLabels(), e.ObjectNew.GetLabels()) {
|
||||
r.handleSAChange(ctx, e.ObjectNew)
|
||||
}
|
||||
},
|
||||
DeleteFunc: func(ctx context.Context, e event.TypedDeleteEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
r.handleSAChange(ctx, e.Object)
|
||||
},
|
||||
}).
|
||||
Complete(r)
|
||||
if crbErr != nil {
|
||||
err = errors.Join(err, crbErr)
|
||||
}
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// Reconcile serves both required ClusterRole and ClusterRoleBinding resources: that's ok, we're watching for multiple
|
||||
@@ -71,8 +88,8 @@ func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
break
|
||||
}
|
||||
|
||||
if err = r.EnsureClusterRoleBindings(ctx); err != nil {
|
||||
r.Log.Error(err, "Reconciliation for ClusterRoleBindings failed")
|
||||
if err = r.EnsureClusterRoleBindingsProvisioner(ctx); err != nil {
|
||||
r.Log.Error(err, "Reconciliation for ClusterRoleBindings (Provisioner) failed")
|
||||
|
||||
break
|
||||
}
|
||||
@@ -82,39 +99,55 @@ func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (r *Manager) EnsureClusterRoleBindings(ctx context.Context) (err error) {
|
||||
func (r *Manager) EnsureClusterRoleBindingsProvisioner(ctx context.Context) error {
|
||||
crb := &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: ProvisionerRoleName,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: ProvisionerRoleName},
|
||||
}
|
||||
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, crb, func() (err error) {
|
||||
crb.RoleRef = provisionerClusterRoleBinding.RoleRef
|
||||
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, crb, func() error {
|
||||
crb.RoleRef = provisionerClusterRoleBinding.RoleRef
|
||||
crb.Subjects = nil
|
||||
|
||||
crb.Subjects = []rbacv1.Subject{}
|
||||
for _, group := range r.Configuration.UserGroups() {
|
||||
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
|
||||
Kind: rbacv1.GroupKind,
|
||||
Name: group,
|
||||
})
|
||||
}
|
||||
|
||||
for _, group := range r.Configuration.UserGroups() {
|
||||
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
|
||||
Kind: "Group",
|
||||
Name: group,
|
||||
})
|
||||
}
|
||||
for _, user := range r.Configuration.UserNames() {
|
||||
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
|
||||
Kind: rbacv1.UserKind,
|
||||
Name: user,
|
||||
})
|
||||
}
|
||||
|
||||
for _, user := range r.Configuration.UserNames() {
|
||||
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
|
||||
Kind: "User",
|
||||
Name: user,
|
||||
})
|
||||
}
|
||||
if r.Configuration.AllowServiceAccountPromotion() {
|
||||
saList := &corev1.ServiceAccountList{}
|
||||
if err := r.Client.List(ctx, saList, client.MatchingLabels{
|
||||
meta.OwnerPromotionLabel: meta.OwnerPromotionLabelTrigger,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
for _, sa := range saList.Items {
|
||||
crb.Subjects = append(crb.Subjects, rbacv1.Subject{
|
||||
Kind: rbacv1.ServiceAccountKind,
|
||||
Name: sa.Name,
|
||||
Namespace: sa.Namespace,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Manager) EnsureClusterRole(ctx context.Context, roleName string) (err error) {
|
||||
@@ -135,7 +168,7 @@ func (r *Manager) EnsureClusterRole(ctx context.Context, roleName string) (err e
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// Start is the Runnable function triggered upon Manager start-up to perform the first RBAC reconciliation
|
||||
@@ -156,7 +189,7 @@ func (r *Manager) Start(ctx context.Context) error {
|
||||
|
||||
r.Log.Info("setting up ClusterRoleBindings")
|
||||
|
||||
if err := r.EnsureClusterRoleBindings(ctx); err != nil {
|
||||
if err := r.EnsureClusterRoleBindingsProvisioner(ctx); err != nil {
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
return nil
|
||||
}
|
||||
@@ -166,3 +199,30 @@ func (r *Manager) Start(ctx context.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Manager) handleSAChange(ctx context.Context, obj client.Object) {
|
||||
if !r.Configuration.AllowServiceAccountPromotion() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.EnsureClusterRoleBindingsProvisioner(ctx); err != nil {
|
||||
r.Log.Error(err, "cannot update ClusterRoleBinding upon ServiceAccount event")
|
||||
}
|
||||
}
|
||||
|
||||
func promotionLabelsChanged(oldLabels, newLabels map[string]string) bool {
|
||||
keys := []string{
|
||||
meta.OwnerPromotionLabel,
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
oldVal, oldOK := oldLabels[key]
|
||||
newVal, newOK := newLabels[key]
|
||||
|
||||
if oldOK != newOK || oldVal != newVal {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func (r resourceClaimController) Reconcile(ctx context.Context, request ctrl.Req
|
||||
|
||||
log.Error(err, "Error reading the object")
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Ensuring the Quota Status
|
||||
@@ -291,5 +291,5 @@ func updateStatusAndEmitEvent(
|
||||
claim.Status.Condition.Message,
|
||||
)
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func (r resourcePoolController) Reconcile(ctx context.Context, request ctrl.Requ
|
||||
|
||||
log.Error(err, "Error reading the object")
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ResourceQuota Reconciliation
|
||||
@@ -298,7 +298,7 @@ func (r *resourcePoolController) canClaimWithinNamespace(
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return res
|
||||
}
|
||||
|
||||
// Handles exhaustions when a exhaustion was already declared in the given map.
|
||||
@@ -336,7 +336,7 @@ func (r *resourcePoolController) handleClaimOrderedExhaustion(
|
||||
return queued, updateStatusAndEmitEvent(ctx, r.Client, r.recorder, claim, cond)
|
||||
}
|
||||
|
||||
return
|
||||
return queued, err
|
||||
}
|
||||
|
||||
func (r *resourcePoolController) handleClaimResourceExhaustion(
|
||||
@@ -399,12 +399,12 @@ func (r *resourcePoolController) handleClaimToPoolBinding(
|
||||
cond.Message = "Claimed resources"
|
||||
|
||||
if err = updateStatusAndEmitEvent(ctx, r.Client, r.recorder, claim, cond); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
pool.AddClaimToStatus(claim)
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// Attempts to garbage collect a ResourceQuota resource.
|
||||
@@ -571,7 +571,7 @@ func (r *resourcePoolController) gatherMatchingNamespaces(
|
||||
seenNamespaces := make(map[string]struct{})
|
||||
|
||||
if !pool.DeletionTimestamp.IsZero() {
|
||||
return
|
||||
return namespaces, err
|
||||
}
|
||||
|
||||
for _, selector := range pool.Spec.Selectors {
|
||||
@@ -597,7 +597,7 @@ func (r *resourcePoolController) gatherMatchingNamespaces(
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return namespaces, err
|
||||
}
|
||||
|
||||
// Get Currently selected claims for the resourcepool.
|
||||
|
||||
@@ -5,12 +5,15 @@ package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/client-go/util/retry"
|
||||
@@ -20,6 +23,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/metrics"
|
||||
)
|
||||
|
||||
@@ -43,7 +47,6 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager) error {
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
//nolint:nakedret
|
||||
func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ctrl.Result, err error) {
|
||||
r.Log = r.Log.WithValues("Request.Name", request.Name)
|
||||
// Fetch the Tenant instance
|
||||
@@ -53,98 +56,82 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
|
||||
r.Log.Info("Request object not found, could have been deleted after reconcile request")
|
||||
|
||||
// If tenant was deleted or cannot be found, clean up metrics
|
||||
r.Metrics.DeleteAllMetrics(request.Name)
|
||||
r.Metrics.DeleteAllMetricsForTenant(request.Name)
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
r.Log.Error(err, "Error reading the object")
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
|
||||
preRecNamespaces := instance.Status.Namespaces
|
||||
defer func() {
|
||||
r.syncTenantStatusMetrics(instance)
|
||||
|
||||
// Ensuring the Tenant Status
|
||||
if err = r.updateTenantStatus(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot update Tenant status")
|
||||
if uerr := r.updateTenantStatus(ctx, instance, err); uerr != nil {
|
||||
err = fmt.Errorf("cannot update tenant status: %w", uerr)
|
||||
|
||||
return
|
||||
}
|
||||
// Ensuring Metadata
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// Ensuring Metadata.
|
||||
if err = r.ensureMetadata(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot ensure metadata")
|
||||
err = fmt.Errorf("cannot ensure metadata: %w", err)
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Ensuring ResourceQuota
|
||||
r.Log.Info("Ensuring limit resources count is updated")
|
||||
|
||||
if err = r.syncCustomResourceQuotaUsages(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot count limited resources")
|
||||
err = fmt.Errorf("cannot count limited resources: %w", err)
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
// Ensuring all namespaces are collected
|
||||
r.Log.Info("Ensuring all Namespaces are collected")
|
||||
|
||||
if err = r.collectNamespaces(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot collect Namespace resources")
|
||||
|
||||
return
|
||||
}
|
||||
// Ensuring Status metrics are exposed
|
||||
r.Log.Info("Ensuring all status metrics are exposed")
|
||||
r.syncStatusMetrics(instance, preRecNamespaces)
|
||||
|
||||
// Ensuring Namespace metadata
|
||||
// Reconcile Namespaces
|
||||
r.Log.Info("Starting processing of Namespaces", "items", len(instance.Status.Namespaces))
|
||||
|
||||
if err = r.syncNamespaces(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync Namespace items")
|
||||
if err = r.reconcileNamespaces(ctx, instance); err != nil {
|
||||
err = fmt.Errorf("namespace(s) had reconciliation errors")
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Ensuring NetworkPolicy resources
|
||||
r.Log.Info("Starting processing of Network Policies")
|
||||
|
||||
if err = r.syncNetworkPolicies(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync NetworkPolicy items")
|
||||
err = fmt.Errorf("cannot sync networkPolicy items: %w", err)
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
// Ensuring LimitRange resources
|
||||
r.Log.Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges.Items))
|
||||
|
||||
if err = r.syncLimitRanges(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync LimitRange items")
|
||||
err = fmt.Errorf("cannot sync limitrange items: %w", err)
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
// Ensuring ResourceQuota resources
|
||||
r.Log.Info("Starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota.Items))
|
||||
|
||||
if err = r.syncResourceQuotas(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync ResourceQuota items")
|
||||
err = fmt.Errorf("cannot sync resourcequota items: %w", err)
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
// Ensuring RoleBinding resources
|
||||
r.Log.Info("Ensuring RoleBindings for Owners and Tenant")
|
||||
|
||||
if err = r.syncRoleBindings(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync RoleBindings items")
|
||||
err = fmt.Errorf("cannot sync rolebindings items: %w", err)
|
||||
|
||||
return
|
||||
}
|
||||
// Ensuring Namespace count
|
||||
r.Log.Info("Ensuring Namespace count")
|
||||
|
||||
if err = r.ensureNamespaceCount(ctx, instance); err != nil {
|
||||
r.Log.Error(err, "Cannot sync Namespace count")
|
||||
|
||||
return
|
||||
return result, err
|
||||
}
|
||||
|
||||
r.Log.Info("Tenant reconciling completed")
|
||||
@@ -152,14 +139,40 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
func (r *Manager) updateTenantStatus(ctx context.Context, tnt *capsulev1beta2.Tenant) error {
|
||||
func (r *Manager) updateTenantStatus(ctx context.Context, tnt *capsulev1beta2.Tenant, reconcileError error) error {
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
||||
if tnt.Spec.Cordoned {
|
||||
tnt.Status.State = capsulev1beta2.TenantStateCordoned
|
||||
} else {
|
||||
tnt.Status.State = capsulev1beta2.TenantStateActive
|
||||
latest := &capsulev1beta2.Tenant{}
|
||||
if err = r.Get(ctx, types.NamespacedName{Name: tnt.GetName()}, latest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.Client.Status().Update(ctx, tnt)
|
||||
latest.Status = tnt.Status
|
||||
|
||||
// Set Ready Condition
|
||||
readyCondition := meta.NewReadyCondition(tnt)
|
||||
if reconcileError != nil {
|
||||
readyCondition.Message = reconcileError.Error()
|
||||
readyCondition.Status = metav1.ConditionFalse
|
||||
readyCondition.Reason = meta.FailedReason
|
||||
}
|
||||
|
||||
latest.Status.Conditions.UpdateConditionByType(readyCondition)
|
||||
|
||||
// Set Cordoned Condition
|
||||
cordonedCondition := meta.NewCordonedCondition(tnt)
|
||||
|
||||
if tnt.Spec.Cordoned {
|
||||
latest.Status.State = capsulev1beta2.TenantStateCordoned
|
||||
|
||||
cordonedCondition.Reason = meta.CordonedReason
|
||||
cordonedCondition.Message = "Tenant is cordoned"
|
||||
cordonedCondition.Status = metav1.ConditionTrue
|
||||
} else {
|
||||
latest.Status.State = capsulev1beta2.TenantStateActive
|
||||
}
|
||||
|
||||
latest.Status.Conditions.UpdateConditionByType(cordonedCondition)
|
||||
|
||||
return r.Client.Status().Update(ctx, latest)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ package tenant
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
capsuleapi "github.com/projectcapsule/capsule/pkg/api"
|
||||
)
|
||||
@@ -17,7 +19,13 @@ func (r *Manager) ensureMetadata(ctx context.Context, tnt *capsulev1beta2.Tenant
|
||||
tnt.Labels = make(map[string]string)
|
||||
}
|
||||
|
||||
tnt.Labels[capsuleapi.TenantNameLabel] = tnt.Name
|
||||
if v, ok := tnt.Labels[capsuleapi.TenantNameLabel]; ok && v == tnt.Name {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.Update(ctx, tnt)
|
||||
if err := r.Update(ctx, tnt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.Get(ctx, types.NamespacedName{Name: tnt.GetName()}, tnt)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ import (
|
||||
//
|
||||
// In case of Namespace-scoped Resource Budget, we're just replicating the resources across all registered Namespaces.
|
||||
|
||||
//nolint:nakedret
|
||||
func (r *Manager) syncResourceQuotas(ctx context.Context, tenant *capsulev1beta2.Tenant) (err error) { //nolint:gocognit
|
||||
// getting ResourceQuota labels for the mutateFn
|
||||
var tenantLabel, typeLabel string
|
||||
@@ -175,16 +174,16 @@ func (r *Manager) syncResourceQuotas(ctx context.Context, tenant *capsulev1beta2
|
||||
if scopeErr = r.resourceQuotasUpdate(ctx, name, quantity, toKeep, resourceQuota.Hard[name], list.Items...); scopeErr != nil {
|
||||
r.Log.Error(scopeErr, "cannot proceed with outer ResourceQuota")
|
||||
|
||||
return
|
||||
return scopeErr
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return scopeErr
|
||||
})
|
||||
}
|
||||
// Waiting the update of all ResourceQuotas
|
||||
if err = group.Wait(); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
// getting requested ResourceQuota keys
|
||||
@@ -207,7 +206,6 @@ func (r *Manager) syncResourceQuotas(ctx context.Context, tenant *capsulev1beta2
|
||||
return group.Wait()
|
||||
}
|
||||
|
||||
//nolint:nakedret
|
||||
func (r *Manager) syncResourceQuota(ctx context.Context, tenant *capsulev1beta2.Tenant, namespace string, keys []string) (err error) {
|
||||
// getting ResourceQuota labels for the mutateFn
|
||||
var tenantLabel, typeLabel string
|
||||
@@ -264,7 +262,7 @@ func (r *Manager) syncResourceQuota(ctx context.Context, tenant *capsulev1beta2.
|
||||
r.Log.Info("Resource Quota sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +293,7 @@ func (r *Manager) resourceQuotasUpdate(ctx context.Context, resourceName corev1.
|
||||
group.Go(func() (err error) {
|
||||
found := &corev1.ResourceQuota{}
|
||||
if err = r.Get(ctx, types.NamespacedName{Namespace: rq.Namespace, Name: rq.Name}, found); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() (retryErr error) {
|
||||
@@ -305,12 +303,19 @@ func (r *Manager) resourceQuotasUpdate(ctx context.Context, resourceName corev1.
|
||||
if found.Annotations == nil {
|
||||
found.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
if found.Labels == nil {
|
||||
found.Labels = make(map[string]string, len(rq.Labels))
|
||||
}
|
||||
|
||||
// Pruning the Capsule quota annotations:
|
||||
// if the ResourceQuota is updated by removing some objects,
|
||||
// we could still have left-overs which could be misleading.
|
||||
// This will not lead to a reconciliation loop since the whole code is idempotent.
|
||||
for k := range found.Annotations {
|
||||
if (strings.HasPrefix(k, capsulev1beta2.HardCapsuleQuotaAnnotation) || strings.HasPrefix(k, capsulev1beta2.UsedCapsuleQuotaAnnotation)) && !annotationsToKeep.Has(k) {
|
||||
if (strings.HasPrefix(k, capsulev1beta2.HardCapsuleQuotaAnnotation) ||
|
||||
strings.HasPrefix(k, capsulev1beta2.UsedCapsuleQuotaAnnotation)) &&
|
||||
(annotationsToKeep == nil || !annotationsToKeep.Has(k)) {
|
||||
delete(found.Annotations, k)
|
||||
}
|
||||
}
|
||||
@@ -323,8 +328,14 @@ func (r *Manager) resourceQuotasUpdate(ctx context.Context, resourceName corev1.
|
||||
if limitKey, keyErr := capsulev1beta2.HardQuotaFor(resourceName); keyErr == nil {
|
||||
found.Annotations[limitKey] = limit.String()
|
||||
}
|
||||
|
||||
// Updating the Resource according to the actual.Cmp result
|
||||
found.Spec.Hard = rq.Spec.Hard
|
||||
if rq.Spec.Hard != nil {
|
||||
found.Spec.Hard = rq.Spec.Hard.DeepCopy()
|
||||
} else {
|
||||
// Ensure it’s nil (or empty) consistently
|
||||
found.Spec.Hard = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
@@ -155,7 +165,7 @@ func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsule
|
||||
r.Log.Info(fmt.Sprintf("RoleBinding sync result: %s", string(res)), "name", target.Name, "namespace", target.Namespace)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ func (r *Manager) pruningResources(ctx context.Context, ns string, keys []string
|
||||
var capsuleLabel string
|
||||
|
||||
if capsuleLabel, err = capsulev1beta2.GetTypeLabel(obj); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
selector := labels.NewSelector()
|
||||
@@ -31,7 +31,7 @@ func (r *Manager) pruningResources(ctx context.Context, ns string, keys []string
|
||||
var exists *labels.Requirement
|
||||
|
||||
if exists, err = labels.NewRequirement(capsuleLabel, selection.Exists, []string{}); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
selector = selector.Add(*exists)
|
||||
|
||||
@@ -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.26.0
|
||||
github.com/onsi/gomega v1.38.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.23.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/valyala/fasttemplate v1.2.2
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/sync v0.16.0
|
||||
k8s.io/api v0.34.0
|
||||
k8s.io/apiextensions-apiserver v0.34.0
|
||||
k8s.io/apimachinery v0.34.0
|
||||
k8s.io/client-go v0.34.0
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
|
||||
sigs.k8s.io/cluster-api v1.11.1
|
||||
sigs.k8s.io/controller-runtime v0.22.0
|
||||
sigs.k8s.io/gateway-api v1.3.0
|
||||
golang.org/x/sync v0.17.0
|
||||
k8s.io/api v0.34.1
|
||||
k8s.io/apiextensions-apiserver v0.34.1
|
||||
k8s.io/apimachinery v0.34.1
|
||||
k8s.io/apiserver v0.34.1
|
||||
k8s.io/client-go v0.34.1
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
|
||||
sigs.k8s.io/cluster-api v1.11.2
|
||||
sigs.k8s.io/controller-runtime v0.22.3
|
||||
sigs.k8s.io/gateway-api v1.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -59,7 +60,6 @@ require (
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
@@ -68,29 +68,29 @@ require (
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.0 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/term v0.34.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/oauth2 v0.31.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250902184714-7fc278399c7f // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
|
||||
72
go.sum
72
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,10 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo/v2 v2.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY=
|
||||
github.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk=
|
||||
github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw=
|
||||
github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE=
|
||||
github.com/onsi/ginkgo/v2 v2.26.0 h1:1J4Wut1IlYZNEAWIV3ALrT9NfiaGW2cDCJQSFQMs/gE=
|
||||
github.com/onsi/ginkgo/v2 v2.26.0/go.mod h1:qhEywmzWTBUY88kfO0BRvX4py7scov9yR+Az2oavUzw=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
@@ -147,14 +147,12 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/common v0.66.0 h1:K/rJPHrG3+AoQs50r2+0t7zMnMzek2Vbv31OFVsMeVY=
|
||||
github.com/prometheus/common v0.66.0/go.mod h1:Ux6NtV1B4LatamKE63tJBntoxD++xmtI/lK0VtEplN4=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
@@ -216,6 +214,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -223,36 +223,53 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
|
||||
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
@@ -273,6 +290,8 @@ google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@@ -280,43 +299,60 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE=
|
||||
k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug=
|
||||
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
|
||||
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
|
||||
k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc=
|
||||
k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0=
|
||||
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
|
||||
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
|
||||
k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0=
|
||||
k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
|
||||
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apiserver v0.34.0 h1:Z51fw1iGMqN7uJ1kEaynf2Aec1Y774PqU+FVWCFV3Jg=
|
||||
k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ=
|
||||
k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
|
||||
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
|
||||
k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo=
|
||||
k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY=
|
||||
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
|
||||
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
|
||||
k8s.io/cluster-bootstrap v0.33.3 h1:u2NTxJ5CFSBFXaDxLQoOWMly8eni31psVso+caq6uwI=
|
||||
k8s.io/cluster-bootstrap v0.33.3/go.mod h1:p970f8u8jf273zyQ5raD8WUu2XyAl0SAWOY82o7i/ds=
|
||||
k8s.io/component-base v0.34.0 h1:bS8Ua3zlJzapklsB1dZgjEJuJEeHjj8yTu1gxE2zQX8=
|
||||
k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg=
|
||||
k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 h1:liMHz39T5dJO1aOKHLvwaCjDbf07wVh6yaUlTpunnkE=
|
||||
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/kube-openapi v0.0.0-20250902184714-7fc278399c7f h1:wyRlmLgBSXi3kgawro8klrMRljXeRo1HFkQRs+meYfs=
|
||||
k8s.io/kube-openapi v0.0.0-20250902184714-7fc278399c7f/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0=
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||
sigs.k8s.io/cluster-api v1.11.0 h1:4ZqKxjhdP3F/vvHMd675rGsDrT/siggnFPt5eKQ8nkI=
|
||||
sigs.k8s.io/cluster-api v1.11.0/go.mod h1:gGmNlHrtJe3z0YV3J6JRy5Rwh9SfzokjQaS+Fv3DBPE=
|
||||
sigs.k8s.io/cluster-api v1.11.1 h1:7CyGCTxv1p3Y2kRe1ljTj/w4TcdIdWNj0CTBc4i1aBo=
|
||||
sigs.k8s.io/cluster-api v1.11.1/go.mod h1:zyrjgJ5RbXhwKcAdUlGPNK5YOHpcmxXvur+5I8lkMUQ=
|
||||
sigs.k8s.io/controller-runtime v0.22.0 h1:mTOfibb8Hxwpx3xEkR56i7xSjB+nH4hZG37SrlCY5e0=
|
||||
sigs.k8s.io/controller-runtime v0.22.0/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY=
|
||||
sigs.k8s.io/cluster-api v1.11.2 h1:uAczaBavU5Y6aDgyoXWtq28k1kalpSZnVItwXHusw1c=
|
||||
sigs.k8s.io/cluster-api v1.11.2/go.mod h1:C1gJVAjMXRG+M+djjGYNkoi5kBMhFnOUI9QqZDAtMms=
|
||||
sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg=
|
||||
sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY=
|
||||
sigs.k8s.io/controller-runtime v0.22.2 h1:cK2l8BGWsSWkXz09tcS4rJh95iOLney5eawcK5A33r4=
|
||||
sigs.k8s.io/controller-runtime v0.22.2/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y=
|
||||
sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
sigs.k8s.io/gateway-api v1.3.0 h1:q6okN+/UKDATola4JY7zXzx40WO4VISk7i9DIfOvr9M=
|
||||
sigs.k8s.io/gateway-api v1.3.0/go.mod h1:d8NV8nJbaRbEKem+5IuxkL8gJGOZ+FJ+NvOIltV8gDk=
|
||||
sigs.k8s.io/gateway-api v1.4.0 h1:ZwlNM6zOHq0h3WUX2gfByPs2yAEsy/EenYJB78jpQfQ=
|
||||
sigs.k8s.io/gateway-api v1.4.0/go.mod h1:AR5RSqciWP98OPckEjOjh2XJhAe2Na4LHyXD2FUY7Qk=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -46,7 +46,7 @@ func (c CapsuleCA) CACertificatePem() (b *bytes.Buffer, err error) {
|
||||
|
||||
crtBytes, err = x509.CreateCertificate(rand.Reader, c.certificate, c.certificate, &c.key.PublicKey, c.key)
|
||||
if err != nil {
|
||||
return
|
||||
return b, err
|
||||
}
|
||||
|
||||
b = new(bytes.Buffer)
|
||||
@@ -111,7 +111,7 @@ func GenerateCertificateAuthority() (s *CapsuleCA, err error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
return s, err
|
||||
}
|
||||
|
||||
func GetCertificateFromBytes(certBytes []byte) (*x509.Certificate, error) {
|
||||
@@ -144,7 +144,6 @@ func GetCertificateWithPrivateKeyFromBytes(certBytes, keyBytes []byte) (*x509.Ce
|
||||
return cert, key, nil
|
||||
}
|
||||
|
||||
//nolint:nakedret
|
||||
func (c *CapsuleCA) GenerateCertificate(opts CertificateOptions) (certificatePem *bytes.Buffer, certificateKey *bytes.Buffer, err error) {
|
||||
var certPrivKey *rsa.PrivateKey
|
||||
|
||||
@@ -185,7 +184,7 @@ func (c *CapsuleCA) GenerateCertificate(opts CertificateOptions) (certificatePem
|
||||
Bytes: certBytes,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
return certificatePem, certificateKey, err
|
||||
}
|
||||
|
||||
certificateKey = new(bytes.Buffer)
|
||||
@@ -195,8 +194,8 @@ func (c *CapsuleCA) GenerateCertificate(opts CertificateOptions) (certificatePem
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
return certificatePem, certificateKey, err
|
||||
}
|
||||
|
||||
return
|
||||
return certificatePem, certificateKey, err
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ func NewCapsuleConfiguration(ctx context.Context, client client.Client, name str
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
panic(errors.Wrap(err, "Cannot retrieve Capsule configuration with name "+name))
|
||||
}
|
||||
|
||||
@@ -69,6 +70,10 @@ func (c *capsuleConfiguration) EnableTLSConfiguration() bool {
|
||||
return c.retrievalFn().Spec.EnableTLSReconciler
|
||||
}
|
||||
|
||||
func (c *capsuleConfiguration) AllowServiceAccountPromotion() bool {
|
||||
return c.retrievalFn().Spec.AllowServiceAccountPromotion
|
||||
}
|
||||
|
||||
func (c *capsuleConfiguration) MutatingWebhookConfigurationName() (name string) {
|
||||
return c.retrievalFn().Spec.CapsuleResources.MutatingWebhookConfigurationName
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ type Configuration interface {
|
||||
// EnableTLSConfiguration enabled the TLS reconciler, responsible for creating CA and TLS certificate required
|
||||
// for the CRD conversion and webhooks.
|
||||
EnableTLSConfiguration() bool
|
||||
AllowServiceAccountPromotion() bool
|
||||
TLSSecretName() string
|
||||
MutatingWebhookConfigurationName() string
|
||||
ValidatingWebhookConfigurationName() string
|
||||
|
||||
@@ -50,6 +50,6 @@ func (s HostnamePath) Func() client.IndexerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return entries
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
const (
|
||||
// ReadyCondition indicates the resource is ready and fully reconciled.
|
||||
ReadyCondition string = "Ready"
|
||||
CordonedCondition string = "Cordoned"
|
||||
NotReadyCondition string = "NotReady"
|
||||
|
||||
AssignedCondition string = "Assigned"
|
||||
@@ -19,11 +20,105 @@ const (
|
||||
// FailedReason indicates a condition or event observed a failure (Claim Rejected).
|
||||
SucceededReason string = "Succeeded"
|
||||
FailedReason string = "Failed"
|
||||
ActiveReason string = "Active"
|
||||
CordonedReason string = "Cordoned"
|
||||
PoolExhaustedReason string = "PoolExhausted"
|
||||
QueueExhaustedReason string = "QueueExhausted"
|
||||
NamespaceExhaustedReason string = "NamespaceExhausted"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
|
||||
type ConditionList []Condition
|
||||
|
||||
// Adds a condition by type.
|
||||
func (c *ConditionList) GetConditionByType(conditionType string) *Condition {
|
||||
for i := range *c {
|
||||
if (*c)[i].Type == conditionType {
|
||||
return &(*c)[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Adds a condition by type.
|
||||
func (c *ConditionList) UpdateConditionByType(condition Condition) {
|
||||
for i, cond := range *c {
|
||||
if cond.Type == condition.Type {
|
||||
(*c)[i].UpdateCondition(condition)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
*c = append(*c, condition)
|
||||
}
|
||||
|
||||
// Removes a condition by type.
|
||||
func (c *ConditionList) RemoveConditionByType(condition Condition) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
|
||||
filtered := make(ConditionList, 0, len(*c))
|
||||
|
||||
for _, cond := range *c {
|
||||
if cond.Type != condition.Type {
|
||||
filtered = append(filtered, cond)
|
||||
}
|
||||
}
|
||||
|
||||
*c = filtered
|
||||
}
|
||||
|
||||
// +kubebuilder:object:generate=true
|
||||
type Condition metav1.Condition
|
||||
|
||||
func NewReadyCondition(obj client.Object) Condition {
|
||||
return Condition{
|
||||
Type: ReadyCondition,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: SucceededReason,
|
||||
Message: "reconciled",
|
||||
LastTransitionTime: metav1.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func NewCordonedCondition(obj client.Object) Condition {
|
||||
return Condition{
|
||||
Type: CordonedCondition,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: ActiveReason,
|
||||
Message: "not cordoned",
|
||||
LastTransitionTime: metav1.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Disregards fields like LastTransitionTime and Version, which are not relevant for the API.
|
||||
func (c *Condition) UpdateCondition(condition Condition) (updated bool) {
|
||||
if condition.Type == c.Type &&
|
||||
condition.Status == c.Status &&
|
||||
condition.Reason == c.Reason &&
|
||||
condition.Message == c.Message &&
|
||||
condition.ObservedGeneration == c.ObservedGeneration {
|
||||
return false
|
||||
}
|
||||
|
||||
if condition.Status != c.Status {
|
||||
c.LastTransitionTime = metav1.Now()
|
||||
}
|
||||
|
||||
c.Type = condition.Type
|
||||
c.Status = condition.Status
|
||||
c.Reason = condition.Reason
|
||||
c.Message = condition.Message
|
||||
c.ObservedGeneration = condition.ObservedGeneration
|
||||
c.LastTransitionTime = condition.LastTransitionTime
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func NewBoundCondition(obj client.Object) metav1.Condition {
|
||||
return metav1.Condition{
|
||||
Type: BoundCondition,
|
||||
|
||||
211
pkg/meta/conditions_test.go
Normal file
211
pkg/meta/conditions_test.go
Normal file
@@ -0,0 +1,211 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package meta
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// helper
|
||||
func makeCond(tpe, status, reason, msg string, gen int64) Condition {
|
||||
return Condition{
|
||||
Type: tpe,
|
||||
Status: metav1.ConditionStatus(status),
|
||||
Reason: reason,
|
||||
Message: msg,
|
||||
ObservedGeneration: gen,
|
||||
LastTransitionTime: metav1.NewTime(time.Unix(0, 0)),
|
||||
}
|
||||
}
|
||||
|
||||
func TestConditionList_GetConditionByType(t *testing.T) {
|
||||
t.Run("returns matching condition", func(t *testing.T) {
|
||||
list := ConditionList{
|
||||
makeCond("Ready", "False", "Init", "starting", 1),
|
||||
makeCond("Synced", "True", "Ok", "done", 2),
|
||||
}
|
||||
|
||||
got := list.GetConditionByType("Synced")
|
||||
assert.NotNil(t, got)
|
||||
assert.Equal(t, "Synced", got.Type)
|
||||
assert.Equal(t, metav1.ConditionTrue, got.Status)
|
||||
assert.Equal(t, "Ok", got.Reason)
|
||||
assert.Equal(t, "done", got.Message)
|
||||
})
|
||||
|
||||
t.Run("returns nil when not found", func(t *testing.T) {
|
||||
list := ConditionList{
|
||||
makeCond("Ready", "False", "Init", "starting", 1),
|
||||
}
|
||||
assert.Nil(t, list.GetConditionByType("Missing"))
|
||||
})
|
||||
|
||||
t.Run("returned pointer refers to slice element (not copy)", func(t *testing.T) {
|
||||
list := ConditionList{
|
||||
makeCond("Ready", "False", "Init", "starting", 1),
|
||||
makeCond("Synced", "True", "Ok", "done", 2),
|
||||
}
|
||||
ptr := list.GetConditionByType("Ready")
|
||||
assert.NotNil(t, ptr)
|
||||
|
||||
ptr.Message = "mutated"
|
||||
// This asserts GetConditionByType returns &list[i] (via index),
|
||||
// not &cond where cond is the range variable copy.
|
||||
assert.Equal(t, "mutated", list[0].Message)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConditionList_UpdateConditionByType(t *testing.T) {
|
||||
now := metav1.Now()
|
||||
|
||||
t.Run("updates existing condition in place", func(t *testing.T) {
|
||||
list := ConditionList{
|
||||
makeCond("Ready", "False", "Init", "starting", 1),
|
||||
makeCond("Synced", "True", "Ok", "done", 2),
|
||||
}
|
||||
beforeLen := len(list)
|
||||
|
||||
list.UpdateConditionByType(Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Reconciled",
|
||||
Message: "ready now",
|
||||
ObservedGeneration: 3,
|
||||
LastTransitionTime: now,
|
||||
})
|
||||
|
||||
assert.Equal(t, beforeLen, len(list))
|
||||
got := list.GetConditionByType("Ready")
|
||||
assert.NotNil(t, got)
|
||||
assert.Equal(t, metav1.ConditionTrue, got.Status)
|
||||
assert.Equal(t, "Reconciled", got.Reason)
|
||||
assert.Equal(t, "ready now", got.Message)
|
||||
assert.Equal(t, int64(3), got.ObservedGeneration)
|
||||
})
|
||||
|
||||
t.Run("appends when condition type not present", func(t *testing.T) {
|
||||
list := ConditionList{
|
||||
makeCond("Ready", "True", "Ok", "ready", 1),
|
||||
}
|
||||
beforeLen := len(list)
|
||||
|
||||
list.UpdateConditionByType(Condition{
|
||||
Type: "Synced",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Done",
|
||||
Message: "synced",
|
||||
ObservedGeneration: 2,
|
||||
LastTransitionTime: now,
|
||||
})
|
||||
|
||||
assert.Equal(t, beforeLen+1, len(list))
|
||||
got := list.GetConditionByType("Synced")
|
||||
assert.NotNil(t, got)
|
||||
assert.Equal(t, metav1.ConditionTrue, got.Status)
|
||||
assert.Equal(t, "Done", got.Reason)
|
||||
assert.Equal(t, "synced", got.Message)
|
||||
assert.Equal(t, int64(2), got.ObservedGeneration)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConditionList_RemoveConditionByType(t *testing.T) {
|
||||
t.Run("removes all conditions with matching type", func(t *testing.T) {
|
||||
list := ConditionList{
|
||||
makeCond("A", "True", "x", "m1", 1),
|
||||
makeCond("B", "True", "y", "m2", 1),
|
||||
makeCond("A", "False", "z", "m3", 2),
|
||||
}
|
||||
list.RemoveConditionByType(Condition{Type: "A"})
|
||||
|
||||
assert.Len(t, list, 1)
|
||||
assert.Equal(t, "B", list[0].Type)
|
||||
})
|
||||
|
||||
t.Run("no-op when type not present", func(t *testing.T) {
|
||||
orig := ConditionList{
|
||||
makeCond("A", "True", "x", "m1", 1),
|
||||
}
|
||||
list := append(ConditionList{}, orig...) // copy
|
||||
|
||||
list.RemoveConditionByType(Condition{Type: "Missing"})
|
||||
|
||||
assert.Equal(t, orig, list)
|
||||
})
|
||||
|
||||
t.Run("nil receiver is safe", func(t *testing.T) {
|
||||
var list *ConditionList // nil receiver
|
||||
assert.NotPanics(t, func() {
|
||||
list.RemoveConditionByType(Condition{Type: "X"})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateCondition(t *testing.T) {
|
||||
now := metav1.Now()
|
||||
|
||||
t.Run("no update when all relevant fields match", func(t *testing.T) {
|
||||
c := &Condition{
|
||||
Type: "Ready",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "All good",
|
||||
}
|
||||
|
||||
updated := c.UpdateCondition(Condition{
|
||||
Type: "Ready",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "All good",
|
||||
LastTransitionTime: now,
|
||||
})
|
||||
|
||||
assert.False(t, updated)
|
||||
})
|
||||
|
||||
t.Run("update occurs on message change", func(t *testing.T) {
|
||||
c := &Condition{
|
||||
Type: "Ready",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "Old message",
|
||||
}
|
||||
|
||||
updated := c.UpdateCondition(Condition{
|
||||
Type: "Ready",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "New message",
|
||||
LastTransitionTime: now,
|
||||
})
|
||||
|
||||
assert.True(t, updated)
|
||||
assert.Equal(t, "New message", c.Message)
|
||||
})
|
||||
|
||||
t.Run("update occurs on status change", func(t *testing.T) {
|
||||
c := &Condition{
|
||||
Type: "Ready",
|
||||
Status: "False",
|
||||
Reason: "Pending",
|
||||
Message: "Not ready yet",
|
||||
}
|
||||
|
||||
updated := c.UpdateCondition(Condition{
|
||||
Type: "Ready",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "Ready",
|
||||
LastTransitionTime: now,
|
||||
})
|
||||
|
||||
assert.True(t, updated)
|
||||
assert.Equal(t, "True", string(c.Status))
|
||||
assert.Equal(t, "Success", c.Reason)
|
||||
assert.Equal(t, "Ready", c.Message)
|
||||
})
|
||||
}
|
||||
@@ -12,6 +12,14 @@ import (
|
||||
const (
|
||||
FreezeLabel = "projectcapsule.dev/freeze"
|
||||
FreezeLabelTrigger = "true"
|
||||
|
||||
OwnerPromotionLabel = "owner.projectcapsule.dev/promote"
|
||||
OwnerPromotionLabelTrigger = "true"
|
||||
|
||||
CordonedLabel = "projectcapsule.dev/cordoned"
|
||||
CordonedLabelTrigger = "true"
|
||||
|
||||
ManagedByCapsuleLabel = "capsule.clastix.io/managed-by"
|
||||
)
|
||||
|
||||
func FreezeLabelTriggers(obj client.Object) bool {
|
||||
@@ -22,6 +30,14 @@ func FreezeLabelRemove(obj client.Object) {
|
||||
labelRemove(obj, FreezeLabel)
|
||||
}
|
||||
|
||||
func OwnerPromotionLabelTriggers(obj client.Object) bool {
|
||||
return labelTriggers(obj, OwnerPromotionLabel, OwnerPromotionLabelTrigger)
|
||||
}
|
||||
|
||||
func OwnerPromotionLabelRemove(obj client.Object) {
|
||||
labelRemove(obj, OwnerPromotionLabel)
|
||||
}
|
||||
|
||||
func labelRemove(obj client.Object, anno string) {
|
||||
annotations := obj.GetLabels()
|
||||
|
||||
|
||||
61
pkg/meta/labels_test.go
Normal file
61
pkg/meta/labels_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package meta
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func TestFreezeLabel(t *testing.T) {
|
||||
ns := &corev1.Namespace{}
|
||||
ns.SetLabels(map[string]string{})
|
||||
|
||||
// absent
|
||||
if FreezeLabelTriggers(ns) {
|
||||
t.Errorf("expected FreezeLabelTriggers to be false when label is absent")
|
||||
}
|
||||
|
||||
// set to trigger
|
||||
ns.Labels[FreezeLabel] = FreezeLabelTrigger
|
||||
if !FreezeLabelTriggers(ns) {
|
||||
t.Errorf("expected FreezeLabelTriggers to be true when label is set to trigger")
|
||||
}
|
||||
|
||||
ns.Labels[FreezeLabel] = "false"
|
||||
if FreezeLabelTriggers(ns) {
|
||||
t.Errorf("expected FreezeLabelTriggers to be false when label is not set to trigger")
|
||||
}
|
||||
|
||||
// remove
|
||||
FreezeLabelRemove(ns)
|
||||
if _, ok := ns.Labels[FreezeLabel]; ok {
|
||||
t.Errorf("expected FreezeLabel to be removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerPromotionLabel(t *testing.T) {
|
||||
ns := &corev1.Namespace{}
|
||||
ns.SetLabels(map[string]string{})
|
||||
|
||||
if OwnerPromotionLabelTriggers(ns) {
|
||||
t.Errorf("expected OwnerPromotionLabelTriggers to be false when label is absent")
|
||||
}
|
||||
|
||||
ns.Labels[OwnerPromotionLabel] = OwnerPromotionLabelTrigger
|
||||
if !OwnerPromotionLabelTriggers(ns) {
|
||||
t.Errorf("expected OwnerPromotionLabelTriggers to be true when label is set to trigger")
|
||||
}
|
||||
|
||||
ns.Labels[OwnerPromotionLabel] = "false"
|
||||
if OwnerPromotionLabelTriggers(ns) {
|
||||
t.Errorf("expected OwnerPromotionLabelTriggers to be false when label is not set to trigger")
|
||||
}
|
||||
|
||||
OwnerPromotionLabelRemove(ns)
|
||||
if _, ok := ns.Labels[OwnerPromotionLabel]; ok {
|
||||
t.Errorf("expected OwnerPromotionLabel to be removed")
|
||||
}
|
||||
}
|
||||
47
pkg/meta/zz_generated.deepcopy.go
Normal file
47
pkg/meta/zz_generated.deepcopy.go
Normal file
@@ -0,0 +1,47 @@
|
||||
//go:build !ignore_autogenerated
|
||||
|
||||
// Copyright 2020-2023 Project Capsule Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package meta
|
||||
|
||||
import ()
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Condition) DeepCopyInto(out *Condition) {
|
||||
*out = *in
|
||||
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition.
|
||||
func (in *Condition) DeepCopy() *Condition {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Condition)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in ConditionList) DeepCopyInto(out *ConditionList) {
|
||||
{
|
||||
in := &in
|
||||
*out = make(ConditionList, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConditionList.
|
||||
func (in ConditionList) DeepCopy() ConditionList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ConditionList)
|
||||
in.DeepCopyInto(out)
|
||||
return *out
|
||||
}
|
||||
@@ -10,7 +10,8 @@ import (
|
||||
|
||||
type TenantRecorder struct {
|
||||
TenantNamespaceRelationshipGauge *prometheus.GaugeVec
|
||||
TenantCordonedStatusGauge *prometheus.GaugeVec
|
||||
TenantNamespaceConditionGauge *prometheus.GaugeVec
|
||||
TenantConditionGauge *prometheus.GaugeVec
|
||||
TenantNamespaceCounterGauge *prometheus.GaugeVec
|
||||
TenantResourceUsageGauge *prometheus.GaugeVec
|
||||
TenantResourceLimitGauge *prometheus.GaugeVec
|
||||
@@ -30,14 +31,22 @@ func NewTenantRecorder() *TenantRecorder {
|
||||
Namespace: metricsPrefix,
|
||||
Name: "tenant_namespace_relationship",
|
||||
Help: "Mapping metric showing namespace to tenant relationships",
|
||||
}, []string{"tenant", "namespace"},
|
||||
}, []string{"tenant", "target_namespace"},
|
||||
),
|
||||
TenantCordonedStatusGauge: prometheus.NewGaugeVec(
|
||||
TenantNamespaceConditionGauge: prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: metricsPrefix,
|
||||
Name: "tenant_status",
|
||||
Help: "Tenant cordon state indicating if tenant operations are restricted (1) or allowed (0) for resource creation and modification",
|
||||
}, []string{"tenant"},
|
||||
Name: "tenant_namespace_condition",
|
||||
Help: "Provides per namespace within a tenant condition status for each condition",
|
||||
}, []string{"tenant", "target_namespace", "condition"},
|
||||
),
|
||||
|
||||
TenantConditionGauge: prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: metricsPrefix,
|
||||
Name: "tenant_condition",
|
||||
Help: "Provides per tenant condition status for each condition",
|
||||
}, []string{"tenant", "condition"},
|
||||
),
|
||||
TenantNamespaceCounterGauge: prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
@@ -66,13 +75,59 @@ func NewTenantRecorder() *TenantRecorder {
|
||||
func (r *TenantRecorder) Collectors() []prometheus.Collector {
|
||||
return []prometheus.Collector{
|
||||
r.TenantNamespaceRelationshipGauge,
|
||||
r.TenantCordonedStatusGauge,
|
||||
r.TenantNamespaceConditionGauge,
|
||||
r.TenantConditionGauge,
|
||||
r.TenantNamespaceCounterGauge,
|
||||
r.TenantResourceUsageGauge,
|
||||
r.TenantResourceLimitGauge,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TenantRecorder) DeleteAllMetricsForNamespace(namespace string) {
|
||||
r.DeleteNamespaceRelationshipMetrics(namespace)
|
||||
r.DeleteTenantNamespaceConditionMetrics(namespace)
|
||||
}
|
||||
|
||||
// DeleteCondition deletes the condition metrics for the ref.
|
||||
func (r *TenantRecorder) DeleteNamespaceRelationshipMetrics(namespace string) {
|
||||
r.TenantNamespaceRelationshipGauge.DeletePartialMatch(map[string]string{
|
||||
"target_namespace": namespace,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *TenantRecorder) DeleteTenantNamespaceConditionMetrics(namespace string) {
|
||||
r.TenantNamespaceConditionGauge.DeletePartialMatch(map[string]string{
|
||||
"target_namespace": namespace,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *TenantRecorder) DeleteTenantNamespaceConditionMetricByType(namespace string, condition string) {
|
||||
r.TenantNamespaceConditionGauge.DeletePartialMatch(map[string]string{
|
||||
"target_namespace": namespace,
|
||||
"condition": condition,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *TenantRecorder) DeleteAllMetricsForTenant(tenant string) {
|
||||
r.DeleteTenantResourceMetrics(tenant)
|
||||
r.DeleteTenantStatusMetrics(tenant)
|
||||
r.DeleteTenantConditionMetrics(tenant)
|
||||
r.DeleteTenantResourceMetrics(tenant)
|
||||
}
|
||||
|
||||
func (r *TenantRecorder) DeleteTenantConditionMetrics(tenant string) {
|
||||
r.TenantConditionGauge.DeletePartialMatch(map[string]string{
|
||||
"tenant": tenant,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *TenantRecorder) DeleteTenantConditionMetricByType(tenant string, condition string) {
|
||||
r.TenantConditionGauge.DeletePartialMatch(map[string]string{
|
||||
"tenant": tenant,
|
||||
"condition": condition,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteCondition deletes the condition metrics for the ref.
|
||||
func (r *TenantRecorder) DeleteTenantResourceMetrics(tenant string) {
|
||||
r.TenantResourceUsageGauge.DeletePartialMatch(map[string]string{
|
||||
@@ -85,25 +140,13 @@ func (r *TenantRecorder) DeleteTenantResourceMetrics(tenant string) {
|
||||
|
||||
// DeleteCondition deletes the condition metrics for the ref.
|
||||
func (r *TenantRecorder) DeleteTenantStatusMetrics(tenant string) {
|
||||
r.TenantNamespaceCounterGauge.DeletePartialMatch(map[string]string{
|
||||
"tenant": tenant,
|
||||
})
|
||||
r.TenantNamespaceRelationshipGauge.DeletePartialMatch(map[string]string{
|
||||
"tenant": tenant,
|
||||
})
|
||||
r.TenantResourceUsageGauge.DeletePartialMatch(map[string]string{
|
||||
"tenant": tenant,
|
||||
})
|
||||
r.TenantResourceLimitGauge.DeletePartialMatch(map[string]string{
|
||||
r.TenantNamespaceConditionGauge.DeletePartialMatch(map[string]string{
|
||||
"tenant": tenant,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteCondition deletes the condition metrics for the ref.
|
||||
func (r *TenantRecorder) DeleteNamespaceRelationshipMetrics(namespace string) {
|
||||
r.TenantNamespaceRelationshipGauge.DeletePartialMatch(map[string]string{
|
||||
"namespace": namespace,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *TenantRecorder) DeleteAllMetrics(tenant string) {
|
||||
r.DeleteTenantResourceMetrics(tenant)
|
||||
r.DeleteTenantStatusMetrics(tenant)
|
||||
}
|
||||
|
||||
16
pkg/utils/maps.go
Normal file
16
pkg/utils/maps.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2020-2025 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package utils
|
||||
|
||||
func MapMergeNoOverrite(dst, src map[string]string) {
|
||||
if len(src) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range src {
|
||||
if _, exists := dst[k]; !exists {
|
||||
dst[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
98
pkg/utils/maps_test.go
Normal file
98
pkg/utils/maps_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMapMergeNoOverrite_AddsNonOverlapping(t *testing.T) {
|
||||
dst := map[string]string{"a": "1"}
|
||||
src := map[string]string{"b": "2"}
|
||||
|
||||
MapMergeNoOverrite(dst, src)
|
||||
|
||||
if got, want := dst["a"], "1"; got != want {
|
||||
t.Fatalf("dst[a] = %q, want %q", got, want)
|
||||
}
|
||||
if got, want := dst["b"], "2"; got != want {
|
||||
t.Fatalf("dst[b] = %q, want %q", got, want)
|
||||
}
|
||||
if len(dst) != 2 {
|
||||
t.Fatalf("len(dst) = %d, want 2", len(dst))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapMergeNoOverrite_DoesNotOverwriteExisting(t *testing.T) {
|
||||
dst := map[string]string{"a": "1"}
|
||||
src := map[string]string{"a": "X"} // overlapping key
|
||||
|
||||
MapMergeNoOverrite(dst, src)
|
||||
|
||||
if got, want := dst["a"], "1"; got != want {
|
||||
t.Fatalf("dst[a] overwritten: got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapMergeNoOverrite_EmptySrc_NoChange(t *testing.T) {
|
||||
dst := map[string]string{"a": "1"}
|
||||
src := map[string]string{} // empty
|
||||
|
||||
before := make(map[string]string, len(dst))
|
||||
for k, v := range dst {
|
||||
before[k] = v
|
||||
}
|
||||
|
||||
MapMergeNoOverrite(dst, src)
|
||||
|
||||
if !reflect.DeepEqual(dst, before) {
|
||||
t.Fatalf("dst changed with empty src: got %#v, want %#v", dst, before)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapMergeNoOverrite_NilSrc_NoChange(t *testing.T) {
|
||||
dst := map[string]string{"a": "1"}
|
||||
var src map[string]string // nil
|
||||
|
||||
before := make(map[string]string, len(dst))
|
||||
for k, v := range dst {
|
||||
before[k] = v
|
||||
}
|
||||
|
||||
MapMergeNoOverrite(dst, src)
|
||||
|
||||
if !reflect.DeepEqual(dst, before) {
|
||||
t.Fatalf("dst changed with nil src: got %#v, want %#v", dst, before)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapMergeNoOverrite_Idempotent(t *testing.T) {
|
||||
dst := map[string]string{"a": "1"}
|
||||
src := map[string]string{"b": "2"}
|
||||
|
||||
MapMergeNoOverrite(dst, src)
|
||||
first := map[string]string{}
|
||||
for k, v := range dst {
|
||||
first[k] = v
|
||||
}
|
||||
|
||||
// Call again; result should be identical
|
||||
MapMergeNoOverrite(dst, src)
|
||||
|
||||
if !reflect.DeepEqual(dst, first) {
|
||||
t.Fatalf("non-idempotent merge: after second merge got %#v, want %#v", dst, first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapMergeNoOverrite_NilDst_Panics(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Fatalf("expected panic when dst is nil, but did not panic")
|
||||
}
|
||||
}()
|
||||
|
||||
var dst map[string]string // nil destination map
|
||||
src := map[string]string{"a": "1"}
|
||||
|
||||
// Writing to a nil map panics; document current behavior via this test.
|
||||
MapMergeNoOverrite(dst, src)
|
||||
}
|
||||
@@ -14,5 +14,5 @@ func GetOwnersWithKinds(tenant *capsulev1beta2.Tenant) (owners []string) {
|
||||
owners = append(owners, fmt.Sprintf("%s:%s", owner.Kind.String(), owner.Name))
|
||||
}
|
||||
|
||||
return
|
||||
return owners
|
||||
}
|
||||
|
||||
@@ -15,10 +15,6 @@ import (
|
||||
"github.com/projectcapsule/capsule/api/v1beta2"
|
||||
)
|
||||
|
||||
const (
|
||||
CordonedLabel = "projectcapsule.dev/cordoned"
|
||||
)
|
||||
|
||||
func GetTypeLabel(t runtime.Object) (label string, err error) {
|
||||
switch v := t.(type) {
|
||||
case *v1beta1.Tenant, *v1beta2.Tenant:
|
||||
@@ -37,5 +33,5 @@ func GetTypeLabel(t runtime.Object) (label string, err error) {
|
||||
err = fmt.Errorf("type %T is not mapped as Capsule label recognized", v)
|
||||
}
|
||||
|
||||
return
|
||||
return label, err
|
||||
}
|
||||
|
||||
@@ -30,5 +30,5 @@ func (u userGroupList) Find(needle string) (found bool) {
|
||||
|
||||
found = i < len(u) && u[i] == needle
|
||||
|
||||
return
|
||||
return found
|
||||
}
|
||||
|
||||
@@ -112,5 +112,5 @@ func appendHostnameError(spec api.AllowedListSpec) (append string) {
|
||||
append += fmt.Sprintf(", or matching the regex %s", spec.Regex)
|
||||
}
|
||||
|
||||
return
|
||||
return append
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (n NetworkingV1) IngressClass() (res *string) {
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return res
|
||||
}
|
||||
|
||||
func (n NetworkingV1) SetIngressClass(ingressClassName string) {
|
||||
@@ -113,7 +113,7 @@ func (n NetworkingV1Beta1) IngressClass() (res *string) {
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return res
|
||||
}
|
||||
|
||||
func (n NetworkingV1Beta1) SetIngressClass(ingressClassName string) {
|
||||
@@ -187,7 +187,7 @@ func (e Extension) IngressClass() (res *string) {
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return res
|
||||
}
|
||||
|
||||
func (e Extension) SetIngressClass(ingressClassName string) {
|
||||
@@ -252,5 +252,5 @@ func (h HostnamesList) IsStringInList(value string) (ok bool) {
|
||||
i := sort.SearchStrings(h, value)
|
||||
ok = i < h.Len() && h[i] == value
|
||||
|
||||
return
|
||||
return ok
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func FromRequest(req admission.Request, decoder admission.Decoder) (ingress Ingr
|
||||
if req.Kind.Version == "v1" {
|
||||
ingressObj := &networkingv1.Ingress{}
|
||||
if err = decoder.Decode(req, ingressObj); err != nil {
|
||||
return
|
||||
return ingress, err
|
||||
}
|
||||
|
||||
ingress = NetworkingV1{Ingress: ingressObj}
|
||||
@@ -48,14 +48,14 @@ func FromRequest(req admission.Request, decoder admission.Decoder) (ingress Ingr
|
||||
|
||||
ingressObj := &networkingv1beta1.Ingress{}
|
||||
if err = decoder.Decode(req, ingressObj); err != nil {
|
||||
return
|
||||
return ingress, err
|
||||
}
|
||||
|
||||
ingress = NetworkingV1Beta1{Ingress: ingressObj}
|
||||
case "extensions":
|
||||
ingressObj := &extensionsv1beta1.Ingress{}
|
||||
if err = decoder.Decode(req, ingressObj); err != nil {
|
||||
return
|
||||
return ingress, err
|
||||
}
|
||||
|
||||
ingress = Extension{Ingress: ingressObj}
|
||||
@@ -63,5 +63,5 @@ func FromRequest(req admission.Request, decoder admission.Decoder) (ingress Ingr
|
||||
err = fmt.Errorf("cannot recognize type %s", req.Kind.Group)
|
||||
}
|
||||
|
||||
return
|
||||
return ingress, err
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
capsuleutils "github.com/projectcapsule/capsule/pkg/utils"
|
||||
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
@@ -74,16 +76,21 @@ func (h *cordoningLabelHandler) syncNamespaceCordonLabel(ctx context.Context, c
|
||||
}
|
||||
}
|
||||
|
||||
if !tnt.Spec.Cordoned {
|
||||
condition := tnt.Status.Conditions.GetConditionByType(meta.CordonedCondition)
|
||||
if condition == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if condition.Status != metav1.ConditionTrue {
|
||||
return nil
|
||||
}
|
||||
|
||||
labels := ns.GetLabels()
|
||||
if _, ok := labels[capsuleutils.CordonedLabel]; ok {
|
||||
if _, ok := labels[meta.CordonedLabel]; ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
ns.Labels[capsuleutils.CordonedLabel] = "true"
|
||||
ns.Labels[meta.CordonedLabel] = "true"
|
||||
|
||||
marshaled, err := json.Marshal(ns)
|
||||
if err != nil {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
capsuletenant "github.com/projectcapsule/capsule/controllers/tenant"
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
|
||||
)
|
||||
@@ -49,12 +49,29 @@ func (h *metadataHandler) OnCreate(client client.Client, decoder admission.Decod
|
||||
}
|
||||
|
||||
// sync namespace metadata
|
||||
if err := capsuletenant.SyncNamespaceMetadata(tenant, ns); err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
instance := tenant.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{
|
||||
Name: ns.GetName(),
|
||||
UID: ns.GetUID(),
|
||||
})
|
||||
|
||||
return &response
|
||||
if len(instance.Metadata.Labels) == 0 && len(instance.Metadata.Annotations) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
labels := ns.GetLabels()
|
||||
for k, v := range instance.Metadata.Labels {
|
||||
labels[k] = v
|
||||
}
|
||||
|
||||
ns.SetLabels(labels)
|
||||
|
||||
annotations := ns.GetAnnotations()
|
||||
for k, v := range instance.Metadata.Annotations {
|
||||
annotations[k] = v
|
||||
}
|
||||
|
||||
ns.SetAnnotations(annotations)
|
||||
|
||||
marshaled, err := json.Marshal(ns)
|
||||
if err != nil {
|
||||
response := admission.Errored(http.StatusInternalServerError, err)
|
||||
|
||||
@@ -60,7 +60,12 @@ func (h *ownerReferenceHandler) OnUpdate(c client.Client, decoder admission.Deco
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if !h.namespaceIsOwned(oldNs, tntList, req) {
|
||||
ok, err := h.namespaceIsOwned(ctx, c, oldNs, tntList, req)
|
||||
if err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
recorder.Eventf(oldNs, corev1.EventTypeWarning, "OfflimitNamespace", "Namespace %s can not be patched", oldNs.GetName())
|
||||
|
||||
response := admission.Denied("Denied patch request for this namespace")
|
||||
@@ -109,20 +114,25 @@ func (h *ownerReferenceHandler) OnUpdate(c client.Client, decoder admission.Deco
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ownerReferenceHandler) namespaceIsOwned(ns *corev1.Namespace, tenantList *capsulev1beta2.TenantList, req admission.Request) bool {
|
||||
func (h *ownerReferenceHandler) namespaceIsOwned(ctx context.Context, c client.Client, ns *corev1.Namespace, tenantList *capsulev1beta2.TenantList, req admission.Request) (bool, error) {
|
||||
for _, tenant := range tenantList.Items {
|
||||
for _, ownerRef := range ns.OwnerReferences {
|
||||
if !capsuleutils.IsTenantOwnerReference(ownerRef) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ownerRef.UID == tenant.UID && utils.IsTenantOwner(tenant.Spec.Owners, req.UserInfo) {
|
||||
return true
|
||||
ok, err := utils.IsTenantOwner(ctx, c, &tenant, req.UserInfo, h.cfg.AllowServiceAccountPromotion())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if ownerRef.UID == tenant.UID && ok {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (h *ownerReferenceHandler) setOwnerRef(ctx context.Context, req admission.Request, client client.Client, decoder admission.Decoder, recorder record.EventRecorder) *admission.Response {
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
|
||||
v1 "k8s.io/api/authentication/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -19,6 +21,7 @@ import (
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
"github.com/projectcapsule/capsule/pkg/meta"
|
||||
capsuleutils "github.com/projectcapsule/capsule/pkg/utils"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
)
|
||||
@@ -46,7 +49,7 @@ func getNamespaceTenant(
|
||||
cfg configuration.Configuration,
|
||||
recorder record.EventRecorder,
|
||||
) (*capsulev1beta2.Tenant, *admission.Response) {
|
||||
tenant, errResponse := getTenantByLabels(ctx, client, ns, req, recorder)
|
||||
tenant, errResponse := getTenantByLabels(ctx, client, ns, req, cfg, recorder)
|
||||
if errResponse != nil {
|
||||
return nil, errResponse
|
||||
}
|
||||
@@ -67,6 +70,7 @@ func getTenantByLabels(
|
||||
client client.Client,
|
||||
ns *corev1.Namespace,
|
||||
req admission.Request,
|
||||
cfg configuration.Configuration,
|
||||
recorder record.EventRecorder,
|
||||
) (*capsulev1beta2.Tenant, *admission.Response) {
|
||||
ln, err := capsuleutils.GetTypeLabel(&capsulev1beta2.Tenant{})
|
||||
@@ -85,8 +89,15 @@ func getTenantByLabels(
|
||||
|
||||
return nil, &response
|
||||
}
|
||||
// Tenant owner must adhere to user that asked for NS creation
|
||||
if !utils.IsTenantOwner(tnt.Spec.Owners, req.UserInfo) {
|
||||
|
||||
ok, err := utils.IsTenantOwner(ctx, client, tnt, req.UserInfo, cfg.AllowServiceAccountPromotion())
|
||||
if err != nil {
|
||||
response := admission.Errored(http.StatusBadRequest, err)
|
||||
|
||||
return nil, &response
|
||||
}
|
||||
|
||||
if !ok {
|
||||
recorder.Eventf(tnt, corev1.EventTypeWarning, "NonOwnedTenant", "Namespace %s cannot be assigned to the current Tenant", ns.GetName())
|
||||
|
||||
response := admission.Denied("Cannot assign the desired namespace to a non-owned Tenant")
|
||||
@@ -102,6 +113,8 @@ func getTenantByLabels(
|
||||
}
|
||||
|
||||
// getTenantByUserInfo returns tenant list associated with admission request userinfo.
|
||||
//
|
||||
//nolint:nestif
|
||||
func getTenantByUserInfo(
|
||||
ctx context.Context,
|
||||
ns *corev1.Namespace,
|
||||
@@ -141,6 +154,16 @@ func getTenantByUserInfo(
|
||||
}
|
||||
|
||||
tenants = append(tenants, saTntList.Items...)
|
||||
|
||||
if cfg.AllowServiceAccountPromotion() {
|
||||
if tnt, err := resolveServiceAccountActor(ctx, ns, userInfo, clt, cfg); err != nil {
|
||||
response := admission.Errored(http.StatusBadRequest, err)
|
||||
|
||||
return nil, &response
|
||||
} else if tnt != nil {
|
||||
tenants = append(tenants, *tnt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group tenants.
|
||||
@@ -195,6 +218,47 @@ func getTenantByUserInfo(
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func resolveServiceAccountActor(
|
||||
ctx context.Context,
|
||||
ns *corev1.Namespace,
|
||||
userInfo v1.UserInfo,
|
||||
clt client.Client,
|
||||
cfg configuration.Configuration,
|
||||
) (tnt *capsulev1beta2.Tenant, err error) {
|
||||
parts := strings.Split(userInfo.Username, ":")
|
||||
if len(parts) != 4 {
|
||||
return tnt, err
|
||||
}
|
||||
|
||||
namespace, saName := parts[2], parts[3]
|
||||
|
||||
sa := &corev1.ServiceAccount{}
|
||||
if err = clt.Get(ctx, client.ObjectKey{Namespace: namespace, Name: saName}, sa); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return tnt, err
|
||||
}
|
||||
|
||||
return tnt, err
|
||||
}
|
||||
|
||||
if meta.OwnerPromotionLabelTriggers(ns) {
|
||||
return tnt, err
|
||||
}
|
||||
|
||||
tntList := &capsulev1beta2.TenantList{}
|
||||
if err = clt.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", namespace),
|
||||
}); err != nil {
|
||||
return tnt, err
|
||||
}
|
||||
|
||||
if len(tntList.Items) > 0 {
|
||||
tnt = &tntList.Items[0]
|
||||
}
|
||||
|
||||
return tnt, err
|
||||
}
|
||||
|
||||
func validateNamespacePrefix(ns *corev1.Namespace, tenant *capsulev1beta2.Tenant) bool {
|
||||
// Check if ForceTenantPrefix is true
|
||||
if tenant.Spec.ForceTenantPrefix != nil && *tenant.Spec.ForceTenantPrefix {
|
||||
|
||||
@@ -15,15 +15,18 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
capsuleutils "github.com/projectcapsule/capsule/pkg/utils"
|
||||
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type patchHandler struct{}
|
||||
type patchHandler struct {
|
||||
configuration configuration.Configuration
|
||||
}
|
||||
|
||||
func PatchHandler() capsulewebhook.Handler {
|
||||
return &patchHandler{}
|
||||
func PatchHandler(configuration configuration.Configuration) capsulewebhook.Handler {
|
||||
return &patchHandler{configuration: configuration}
|
||||
}
|
||||
|
||||
func (r *patchHandler) OnCreate(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
@@ -66,7 +69,14 @@ func (r *patchHandler) OnUpdate(c client.Client, decoder admission.Decoder, reco
|
||||
return &response
|
||||
}
|
||||
|
||||
if utils.IsTenantOwner(tnt.Spec.Owners, req.UserInfo) {
|
||||
ok, err := utils.IsTenantOwner(ctx, c, tnt, req.UserInfo, r.configuration.AllowServiceAccountPromotion())
|
||||
if err != nil {
|
||||
response := admission.Errored(http.StatusBadRequest, err)
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func (r *handler) handle(ctx context.Context, req admission.Request, client clie
|
||||
|
||||
objectLabel, err := capsuleutils.GetTypeLabel(&networkingv1.NetworkPolicy{})
|
||||
if err != nil {
|
||||
return
|
||||
return allowed, err
|
||||
}
|
||||
|
||||
labels := np.GetLabels()
|
||||
@@ -81,5 +81,5 @@ func (r *handler) handle(ctx context.Context, req admission.Request, client clie
|
||||
allowed = false
|
||||
}
|
||||
|
||||
return
|
||||
return allowed, err
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func appendForbiddenError(spec *capsulev1beta2.ForbiddenListSpec) (append string
|
||||
append += fmt.Sprintf("matching the regex %s", spec.Regex)
|
||||
}
|
||||
|
||||
return
|
||||
return append
|
||||
}
|
||||
|
||||
type nodeLabelForbiddenError struct {
|
||||
|
||||
@@ -13,14 +13,19 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook"
|
||||
"github.com/projectcapsule/capsule/pkg/webhook/utils"
|
||||
)
|
||||
|
||||
type containerRegistryHandler struct{}
|
||||
type containerRegistryHandler struct {
|
||||
configuration configuration.Configuration
|
||||
}
|
||||
|
||||
func ContainerRegistry() capsulewebhook.Handler {
|
||||
return &containerRegistryHandler{}
|
||||
func ContainerRegistry(configuration configuration.Configuration) capsulewebhook.Handler {
|
||||
return &containerRegistryHandler{
|
||||
configuration: configuration,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *containerRegistryHandler) OnCreate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
@@ -42,7 +47,13 @@ func (h *containerRegistryHandler) OnUpdate(c client.Client, decoder admission.D
|
||||
}
|
||||
}
|
||||
|
||||
func (h *containerRegistryHandler) validate(ctx context.Context, c client.Client, decoder admission.Decoder, recorder record.EventRecorder, req admission.Request) *admission.Response {
|
||||
func (h *containerRegistryHandler) validate(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
req admission.Request,
|
||||
) *admission.Response {
|
||||
pod := &corev1.Pod{}
|
||||
if err := decoder.Decode(req, pod); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
@@ -61,34 +72,45 @@ func (h *containerRegistryHandler) validate(ctx context.Context, c client.Client
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
if tnt.Spec.ContainerRegistries != nil {
|
||||
// Evaluate init containers
|
||||
for _, container := range pod.Spec.InitContainers {
|
||||
if response := h.verifyContainerRegistry(recorder, req, container, tnt); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
if tnt.Spec.ContainerRegistries == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Evaluate containers
|
||||
for _, container := range pod.Spec.Containers {
|
||||
if response := h.verifyContainerRegistry(recorder, req, container, tnt); response != nil {
|
||||
return response
|
||||
}
|
||||
for _, container := range pod.Spec.InitContainers {
|
||||
if response := h.verifyContainerRegistry(recorder, req, container.Image, tnt); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
for _, container := range pod.Spec.EphemeralContainers {
|
||||
if response := h.verifyContainerRegistry(recorder, req, container.Image, tnt); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
for _, container := range pod.Spec.Containers {
|
||||
if response := h.verifyContainerRegistry(recorder, req, container.Image, tnt); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *containerRegistryHandler) verifyContainerRegistry(recorder record.EventRecorder, req admission.Request, container corev1.Container, tnt capsulev1beta2.Tenant) *admission.Response {
|
||||
func (h *containerRegistryHandler) verifyContainerRegistry(
|
||||
recorder record.EventRecorder,
|
||||
req admission.Request,
|
||||
image string,
|
||||
tnt capsulev1beta2.Tenant,
|
||||
) *admission.Response {
|
||||
var valid, matched bool
|
||||
|
||||
reg := NewRegistry(container.Image)
|
||||
reg := NewRegistry(image, h.configuration)
|
||||
|
||||
if len(reg.Registry()) == 0 {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "MissingFQCI", "Pod %s/%s is not using a fully qualified container image, cannot enforce registry the current Tenant", req.Namespace, req.Name, reg.Registry())
|
||||
|
||||
response := admission.Denied(NewContainerRegistryForbidden(container.Image, *tnt.Spec.ContainerRegistries).Error())
|
||||
response := admission.Denied(NewContainerRegistryForbidden(image, *tnt.Spec.ContainerRegistries).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
@@ -100,7 +122,7 @@ func (h *containerRegistryHandler) verifyContainerRegistry(recorder record.Event
|
||||
if !valid && !matched {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenContainerRegistry", "Pod %s/%s is using a container hosted on registry %s that is forbidden for the current Tenant", req.Namespace, req.Name, reg.Registry())
|
||||
|
||||
response := admission.Denied(NewContainerRegistryForbidden(container.Image, *tnt.Spec.ContainerRegistries).Error())
|
||||
response := admission.Denied(NewContainerRegistryForbidden(reg.FQCI(), *tnt.Spec.ContainerRegistries).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
@@ -49,5 +49,5 @@ func (f registryClassForbiddenError) Error() (err string) {
|
||||
|
||||
err += strings.Join(extra, " or ")
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
package pod
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/configuration"
|
||||
)
|
||||
|
||||
type registry map[string]string
|
||||
@@ -49,14 +53,46 @@ func (r registry) Tag() string {
|
||||
return res
|
||||
}
|
||||
|
||||
func (r registry) FQCI() string {
|
||||
reg := r.Registry()
|
||||
repo := r.Repository()
|
||||
img := r.Image()
|
||||
tag := r.Tag()
|
||||
|
||||
// If there's no image, nothing to return
|
||||
if img == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ensure repo ends with "/" if set
|
||||
if repo != "" && repo[len(repo)-1] != '/' {
|
||||
repo += "/"
|
||||
}
|
||||
|
||||
// always append tag to image (strip any trailing : from image just in case)
|
||||
// but our Image() already includes the name:tag, so split carefully
|
||||
name := img
|
||||
if tag != "" && !strings.Contains(img, ":") {
|
||||
name = fmt.Sprintf("%s:%s", img, tag)
|
||||
}
|
||||
|
||||
// build: [registry/]repo+image
|
||||
if reg != "" {
|
||||
return fmt.Sprintf("%s/%s%s", reg, repo, name)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s", repo, name)
|
||||
}
|
||||
|
||||
type Registry interface {
|
||||
Registry() string
|
||||
Repository() string
|
||||
Image() string
|
||||
Tag() string
|
||||
FQCI() string
|
||||
}
|
||||
|
||||
func NewRegistry(value string) Registry {
|
||||
func NewRegistry(value string, cfg configuration.Configuration) Registry {
|
||||
reg := make(registry)
|
||||
r := regexp.MustCompile(`((?P<registry>[a-zA-Z0-9-._]+(:\d+)?)\/)?(?P<repository>.*\/)?(?P<image>[a-zA-Z0-9-._]+:(?P<tag>[a-zA-Z0-9-._]+))?`)
|
||||
match := r.FindStringSubmatch(value)
|
||||
|
||||
@@ -25,49 +25,13 @@ func ImagePullPolicy() capsulewebhook.Handler {
|
||||
|
||||
func (r *imagePullPolicy) OnCreate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
pod := &corev1.Pod{}
|
||||
if err := decoder.Decode(req, pod); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tntList := &capsulev1beta2.TenantList{}
|
||||
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace),
|
||||
}); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
// the Pod is not running in a Namespace managed by a Tenant
|
||||
if len(tntList.Items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
policy := NewPullPolicy(&tnt)
|
||||
// if Tenant doesn't enforce the pull policy, exit
|
||||
if policy == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, container := range pod.Spec.Containers {
|
||||
usedPullPolicy := string(container.ImagePullPolicy)
|
||||
|
||||
if !policy.IsPolicySupported(usedPullPolicy) {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenPullPolicy", "Pod %s/%s pull policy %s is forbidden for the current Tenant", req.Namespace, req.Name, usedPullPolicy)
|
||||
|
||||
response := admission.Denied(NewImagePullPolicyForbidden(usedPullPolicy, container.Name, policy.AllowedPullPolicies()).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return r.validate(ctx, c, decoder, recorder, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *imagePullPolicy) OnUpdate(client.Client, admission.Decoder, record.EventRecorder) capsulewebhook.Func {
|
||||
return func(context.Context, admission.Request) *admission.Response {
|
||||
return nil
|
||||
func (r *imagePullPolicy) OnUpdate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
|
||||
return func(ctx context.Context, req admission.Request) *admission.Response {
|
||||
return r.validate(ctx, c, decoder, recorder, req)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,3 +40,73 @@ func (r *imagePullPolicy) OnDelete(client.Client, admission.Decoder, record.Even
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *imagePullPolicy) validate(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
decoder admission.Decoder,
|
||||
recorder record.EventRecorder,
|
||||
req admission.Request,
|
||||
) *admission.Response {
|
||||
pod := &corev1.Pod{}
|
||||
if err := decoder.Decode(req, pod); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
tntList := &capsulev1beta2.TenantList{}
|
||||
if err := c.List(ctx, tntList, client.MatchingFieldsSelector{
|
||||
Selector: fields.OneTermEqualSelector(".status.namespaces", pod.Namespace),
|
||||
}); err != nil {
|
||||
return utils.ErroredResponse(err)
|
||||
}
|
||||
|
||||
if len(tntList.Items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tnt := tntList.Items[0]
|
||||
|
||||
policy := NewPullPolicy(&tnt)
|
||||
if policy == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, container := range pod.Spec.InitContainers {
|
||||
if response := h.verifyPullPolicy(recorder, req, policy, string(container.ImagePullPolicy), container.Name, tnt); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
for _, container := range pod.Spec.EphemeralContainers {
|
||||
if response := h.verifyPullPolicy(recorder, req, policy, string(container.ImagePullPolicy), container.Name, tnt); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
for _, container := range pod.Spec.Containers {
|
||||
if response := h.verifyPullPolicy(recorder, req, policy, string(container.ImagePullPolicy), container.Name, tnt); response != nil {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *imagePullPolicy) verifyPullPolicy(
|
||||
recorder record.EventRecorder,
|
||||
req admission.Request,
|
||||
policy PullPolicy,
|
||||
usedPullPolicy string,
|
||||
container string,
|
||||
tnt capsulev1beta2.Tenant,
|
||||
) *admission.Response {
|
||||
if !policy.IsPolicySupported(usedPullPolicy) {
|
||||
recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenPullPolicy", "Pod %s/%s pull policy %s is forbidden for the current Tenant", req.Namespace, req.Name, usedPullPolicy)
|
||||
|
||||
response := admission.Denied(NewImagePullPolicyForbidden(usedPullPolicy, container, policy.AllowedPullPolicies()).Error())
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user