Compare commits

...

12 Commits

Author SHA1 Message Date
renovate[bot]
cd0675e8a3 chore(deps): update securego/gosec action to v2.22.11 (#1788)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 17:04:07 +01:00
Oliver Bähler
e19575bcbd fix(controller): allow no spaces in template references (#1789)
* fix(controller): decode old object for delete requests

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

* chore: modernize golang

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

* chore: modernize golang

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

* chore: modernize golang

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

* fix(controller): allow no spaces in template references

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

* fix(controller): allow no spaces in template references

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

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2025-12-11 17:03:52 +01:00
Oliver Bähler
c06f54a3a3 fix(controller): decode old object for delete requests (#1787)
* fix(controller): decode old object for delete requests

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

* chore: modernize golang

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

* chore: modernize golang

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

* chore: modernize golang

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

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2025-12-10 18:34:42 +01:00
renovate[bot]
cd5e2a82e1 fix(deps): update module k8s.io/apiextensions-apiserver to v0.34.3 (#1785)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-10 07:59:00 +01:00
renovate[bot]
2583215e8b fix(deps): update module k8s.io/dynamic-resource-allocation to v0.34.3 (#1786)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-10 07:20:14 +01:00
renovate[bot]
8ceb375310 chore(deps): update anchore/sbom-action digest to 43a17d6 (#1781)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-10 06:37:11 +01:00
renovate[bot]
b0e086464d chore(deps): update codecov/codecov-action action to v5.5.2 (#1783)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-10 06:36:44 +01:00
renovate[bot]
ad38a28468 chore(deps): update capsule-proxy docker tag to v0.10.0 (#1782)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 17:06:03 +01:00
renovate[bot]
f44b8b2b29 chore(deps): update github/codeql-action digest to c43362b (#1779)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 16:59:05 +01:00
renovate[bot]
c832f56683 fix(deps): update module golang.org/x/sync to v0.19.0 (#1774)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 16:58:06 +01:00
renovate[bot]
4b35b1e456 chore(deps): update helm release argo-cd to v9.1.7 (#1780)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 14:35:25 +01:00
Oliver Bähler
40cb5bdeeb chore(dev): add local argocd setup (#1778)
* fix(controller): make device and gateway class optional

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

* chore(dev): add local argocd setup

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

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2025-12-09 13:44:40 +01:00
47 changed files with 648 additions and 231 deletions

View File

@@ -52,11 +52,11 @@ jobs:
with:
go-version-file: 'go.mod'
- name: Run Gosec Security Scanner
uses: securego/gosec@6be2b51fd78feca86af91f5186b7964d76cb1256 # v2.22.10
uses: securego/gosec@424fc4cd9c82ea0fd6bee9cd49c2db2c3cc0c93f # v2.22.11
with:
args: '-no-fail -fmt sarif -out gosec.sarif ./...'
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@149d184a5153ea45e6fbcef5588ac7b8c7af9835
uses: github/codeql-action/upload-sarif@c43362b91a940600cde2ebae39ec7a35ad66bdc0
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@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: projectcapsule/capsule

View File

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

View File

@@ -30,7 +30,7 @@ jobs:
timeout-minutes: 5
continue-on-error: true
- uses: creekorful/goreportcard-action@1f35ced8cdac2cba28c9a2f2288a16aacfd507f9 # v1.0
- uses: anchore/sbom-action/download-syft@fbfd9c6c189226748411491745178e0c2017392d
- uses: anchore/sbom-action/download-syft@43a17d6e7add2b5535efe4dcae9952337c479a93
- name: Install Cosign
uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
- name: Run GoReleaser

View File

@@ -150,6 +150,7 @@ dev-setup:
--set 'crds.install=true' \
--set 'crds.exclusive=true'\
--set 'crds.createConfig=true'\
--set "tls.enableController=false"\
--set "webhooks.exclusive=true"\
--set "webhooks.hooks.nodes.enabled=true"\
--set "webhooks.service.url=$${WEBHOOK_URL}" \
@@ -168,6 +169,14 @@ setup-monitoring: dev-setup-fluxcd
dev-setup-monitoring: setup-monitoring
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/host-proxy | envsubst | kubectl apply -f -
dev-setup-argocd: dev-setup-fluxcd
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/argocd | envsubst | kubectl apply -f -
@$(MAKE) wait-for-helmreleases
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/argocd/application | envsubst | kubectl apply -f -
@printf "\n\033[32mAccess ArgoCD:\033[0m\n\n"
@printf " \033[1mkubectl get secret -n argocd argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d\033[0m\n\n"
@printf " \033[1mkubectl port-forward svc/argocd-server 9091:80 -n argocd\033[0m\n\n"
dev-setup-fluxcd:
@$(KUBECTL) kustomize --load-restrictor='LoadRestrictionsNone' hack/distro/fluxcd | envsubst | kubectl apply -f -
@@ -407,7 +416,7 @@ nwa:
$(call go-install-tool,$(NWA),github.com/$(NWA_LOOKUP)@$(NWA_VERSION))
GOLANGCI_LINT := $(LOCALBIN)/golangci-lint
GOLANGCI_LINT_VERSION := v2.5.0
GOLANGCI_LINT_VERSION := v2.7.2
GOLANGCI_LINT_LOOKUP := golangci/golangci-lint
golangci-lint: ## Download golangci-lint locally if necessary.
@test -s $(GOLANGCI_LINT) && $(GOLANGCI_LINT) -h | grep -q $(GOLANGCI_LINT_VERSION) || \

View File

@@ -20,17 +20,21 @@ type TenantSpec struct {
// Specifies the allowed StorageClasses assigned to the Tenant. Capsule assures that all PersistentVolumeClaim resources created in the Tenant can use only one of the allowed StorageClasses. Optional.
StorageClasses *api.AllowedListSpec `json:"storageClasses,omitempty"`
// Specifies options for the Ingress resources, such as allowed hostnames and IngressClass. Optional.
IngressOptions IngressOptions `json:"ingressOptions,omitempty"`
// +optional
IngressOptions IngressOptions `json:"ingressOptions,omitzero"`
// Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
ContainerRegistries *api.AllowedListSpec `json:"containerRegistries,omitempty"`
// Specifies the label to control the placement of pods on a given pool of worker nodes. All namespaces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitempty"`
// +optional
NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitzero"`
// Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
LimitRanges api.LimitRangesSpec `json:"limitRanges,omitempty"`
// +optional
LimitRanges api.LimitRangesSpec `json:"limitRanges,omitzero"`
// Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional.
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
// +optional
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitzero"`
// Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
AdditionalRoleBindings []api.AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
// Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
@@ -50,11 +54,13 @@ type TenantSpec struct {
// Tenant is the Schema for the tenants API.
type Tenant struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec TenantSpec `json:"spec,omitempty"`
Status TenantStatus `json:"status,omitempty"`
Spec TenantSpec `json:"spec"`
// +optional
Status TenantStatus `json:"status,omitzero"`
}
func (in *Tenant) Hub() {}
@@ -64,7 +70,8 @@ func (in *Tenant) Hub() {}
// TenantList contains a list of Tenant.
type TenantList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
// +optional
metav1.ListMeta `json:"metadata,omitzero"`
Items []Tenant `json:"items"`
}

View File

@@ -40,13 +40,14 @@ type CapsuleConfigurationSpec struct {
// Allows to set different name rather than the canonical one for the Capsule configuration objects,
// such as webhook secret or configurations.
// +kubebuilder:default={TLSSecretName:"capsule-tls",mutatingWebhookConfigurationName:"capsule-mutating-webhook-configuration",validatingWebhookConfigurationName:"capsule-validating-webhook-configuration"}
CapsuleResources CapsuleResources `json:"overrides,omitempty"`
// +optional
CapsuleResources CapsuleResources `json:"overrides,omitzero"`
// Allows to set the forbidden metadata for the worker nodes that could be patched by a Tenant.
// This applies only if the Tenant has an active NodeSelector, and the Owner have right to patch their nodes.
NodeMetadata *NodeMetadata `json:"nodeMetadata,omitempty"`
// Toggles the TLS reconciler, the controller that is able to generate CA and certificates for the webhooks
// when not using an already provided CA and certificate, or when these are managed externally with Vault, or cert-manager.
// +kubebuilder:default=true
// +kubebuilder:default=false
EnableTLSReconciler bool `json:"enableTLSReconciler"` //nolint:tagliatelle
// Define entities which can act as Administrators in the capsule construct
// These entities are automatically owners for all existing tenants. Meaning they can add namespaces to any tenant. However they must be specific by using the capsule label
@@ -57,9 +58,11 @@ type CapsuleConfigurationSpec struct {
type NodeMetadata struct {
// Define the labels that a Tenant Owner cannot set for their nodes.
ForbiddenLabels api.ForbiddenListSpec `json:"forbiddenLabels"`
// +optional
ForbiddenLabels api.ForbiddenListSpec `json:"forbiddenLabels,omitzero"`
// Define the annotations that a Tenant Owner cannot set for their nodes.
ForbiddenAnnotations api.ForbiddenListSpec `json:"forbiddenAnnotations"`
// +optional
ForbiddenAnnotations api.ForbiddenListSpec `json:"forbiddenAnnotations,omitzero"`
}
type CapsuleResources struct {
@@ -81,10 +84,12 @@ type CapsuleResources struct {
// CapsuleConfiguration is the Schema for the Capsule configuration API.
type CapsuleConfiguration struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
metav1.TypeMeta `json:",inline"`
Spec CapsuleConfigurationSpec `json:"spec,omitempty"`
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec CapsuleConfigurationSpec `json:"spec"`
}
// +kubebuilder:object:root=true
@@ -92,7 +97,7 @@ type CapsuleConfiguration struct {
// CapsuleConfigurationList contains a list of CapsuleConfiguration.
type CapsuleConfigurationList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []CapsuleConfiguration `json:"items"`
}

View File

@@ -18,9 +18,11 @@ type NamespaceOptions struct {
// 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"`
// Define the labels that a Tenant Owner cannot set for their Namespace resources.
ForbiddenLabels api.ForbiddenListSpec `json:"forbiddenLabels,omitempty"`
// +optional
ForbiddenLabels api.ForbiddenListSpec `json:"forbiddenLabels,omitzero"`
// Define the annotations that a Tenant Owner cannot set for their Namespace resources.
ForbiddenAnnotations api.ForbiddenListSpec `json:"forbiddenAnnotations,omitempty"`
// +optional
ForbiddenAnnotations api.ForbiddenListSpec `json:"forbiddenAnnotations,omitzero"`
// If enabled only metadata from additionalMetadata is reconciled to the namespaces.
//+kubebuilder:default:=false
ManagedMetadataOnly bool `json:"managedMetadataOnly,omitempty"`

View File

@@ -21,9 +21,11 @@ type ResourcePoolStatus struct {
// Namespaces which are considered for claims
Namespaces []string `json:"namespaces,omitempty"`
// Tracks the quotas for the Resource.
Claims ResourcePoolNamespaceClaimsStatus `json:"claims,omitempty"`
// +optional
Claims ResourcePoolNamespaceClaimsStatus `json:"claims,omitzero"`
// Tracks the Usage from Claimed against what has been granted from the pool
Allocation ResourcePoolQuotaStatus `json:"allocation,omitempty"`
// +optional
Allocation ResourcePoolQuotaStatus `json:"allocation,omitzero"`
// Exhaustions from claims associated with the pool
Exhaustions map[string]api.PoolExhaustionResource `json:"exhaustions,omitempty"`
}

View File

@@ -18,10 +18,12 @@ type ResourcePoolSpec struct {
Quota corev1.ResourceQuotaSpec `json:"quota"`
// The Defaults given for each namespace, the default is not counted towards the total allocation
// When you use claims it's recommended to provision Defaults as the prevent the scheduling of any resources
Defaults corev1.ResourceList `json:"defaults,omitempty"`
// +optional
Defaults corev1.ResourceList `json:"defaults,omitzero"`
// Additional Configuration
//+kubebuilder:default:={}
Config ResourcePoolSpecConfiguration `json:"config,omitempty"`
// +optional
Config ResourcePoolSpecConfiguration `json:"config,omitzero"`
}
type ResourcePoolSpecConfiguration struct {
@@ -55,11 +57,15 @@ type ResourcePoolSpecConfiguration struct {
// it's up the group of users within these namespaces, to manage the resources they consume per namespace. Each Resourcepool provisions a ResourceQuotainto all the selected namespaces. Then essentially the ResourcePoolClaims, when they can be assigned to the ResourcePool stack resources on top of that
// ResourceQuota based on the namspace, where the ResourcePoolClaim was made from.
type ResourcePool struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
metav1.TypeMeta `json:",inline"`
Spec ResourcePoolSpec `json:"spec,omitempty"`
Status ResourcePoolStatus `json:"status,omitempty"`
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec ResourcePoolSpec `json:"spec"`
// +optional
Status ResourcePoolStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
@@ -67,7 +73,9 @@ type ResourcePool struct {
// ResourcePoolList contains a list of ResourcePool.
type ResourcePoolList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
// +optional
metav1.ListMeta `json:"metadata,omitzero"`
Items []ResourcePool `json:"items"`
}

View File

@@ -22,9 +22,11 @@ type ResourcePoolClaimSpec struct {
// ResourceQuotaClaimStatus defines the observed state of ResourceQuotaClaim.
type ResourcePoolClaimStatus struct {
// Reference to the GlobalQuota being claimed from
Pool api.StatusNameUID `json:"pool,omitempty"`
// +optional
Pool api.StatusNameUID `json:"pool,omitzero"`
// Condtion for this resource claim
Condition metav1.Condition `json:"condition,omitempty"`
// +optional
Condition metav1.Condition `json:"condition,omitzero"`
}
// +kubebuilder:object:root=true
@@ -37,11 +39,15 @@ type ResourcePoolClaimStatus struct {
// ResourcePoolClaim is the Schema for the resourcepoolclaims API.
type ResourcePoolClaim struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
metav1.TypeMeta `json:",inline"`
Spec ResourcePoolClaimSpec `json:"spec,omitempty"`
Status ResourcePoolClaimStatus `json:"status,omitempty"`
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec ResourcePoolClaimSpec `json:"spec"`
// +optional
Status ResourcePoolClaimStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
@@ -49,7 +55,9 @@ type ResourcePoolClaim struct {
// ResourceQuotaClaimList contains a list of ResourceQuotaClaim.
type ResourcePoolClaimList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
// +optional
metav1.ListMeta `json:"metadata,omitzero"`
Items []ResourcePoolClaim `json:"items"`
}

View File

@@ -58,7 +58,8 @@ type TenantStatusNamespaceMetadata struct {
type TenantAvailableStatus struct {
// Available Class Types within Tenant
Classes TenantAvailableClassesStatus `json:"classes,omitempty"`
// +optional
Classes TenantAvailableClassesStatus `json:"classes,omitzero"`
}
type TenantAvailableClassesStatus struct {

View File

@@ -16,7 +16,8 @@ import (
// TenantSpec defines the desired state of Tenant.
type TenantSpec struct {
// Specify Permissions for the Tenant.
Permissions Permissions `json:"permissions,omitempty"`
// +optional
Permissions Permissions `json:"permissions,omitzero"`
// Specifies the owners of the Tenant.
// Optional
Owners api.OwnerListSpec `json:"owners,omitempty"`
@@ -32,7 +33,8 @@ type TenantSpec struct {
// Optional.
StorageClasses *api.DefaultAllowedListSpec `json:"storageClasses,omitempty"`
// Specifies options for the Ingress resources, such as allowed hostnames and IngressClass. Optional.
IngressOptions IngressOptions `json:"ingressOptions,omitempty"`
// +optional
IngressOptions IngressOptions `json:"ingressOptions,omitzero"`
// Specifies the trusted Image Registries assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed trusted registries. Optional.
ContainerRegistries *api.AllowedListSpec `json:"containerRegistries,omitempty"`
// Specifies the label to control the placement of pods on a given pool of worker nodes. All namespaces created within the Tenant will have the node selector annotation. This annotation tells the Kubernetes scheduler to place pods on the nodes having the selector label. Optional.
@@ -40,13 +42,16 @@ type TenantSpec struct {
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
//
// Specifies the NetworkPolicies assigned to the Tenant. The assigned NetworkPolicies are inherited by any namespace created in the Tenant. Optional.
NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitempty"`
// +optional
NetworkPolicies api.NetworkPolicySpec `json:"networkPolicies,omitzero"`
// Deprecated: Use Tenant Replications instead (https://projectcapsule.dev/docs/replications/)
//
// Specifies the resource min/max usage restrictions to the Tenant. The assigned values are inherited by any namespace created in the Tenant. Optional.
LimitRanges api.LimitRangesSpec `json:"limitRanges,omitempty"`
// +optional
LimitRanges api.LimitRangesSpec `json:"limitRanges,omitzero"`
// Specifies a list of ResourceQuota resources assigned to the Tenant. The assigned values are inherited by any namespace created in the Tenant. The Capsule operator aggregates ResourceQuota at Tenant level, so that the hard quota is never crossed for the given Tenant. This permits the Tenant owner to consume resources in the Tenant regardless of the namespace. Optional.
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitempty"`
// +optional
ResourceQuota api.ResourceQuotaSpec `json:"resourceQuotas,omitzero"`
// Specifies additional RoleBindings assigned to the Tenant. Capsule will ensure that all namespaces in the Tenant always contain the RoleBinding for the given ClusterRole. Optional.
AdditionalRoleBindings []api.AdditionalRoleBindingsSpec `json:"additionalRoleBindings,omitempty"`
// Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
@@ -63,7 +68,8 @@ type TenantSpec struct {
// Specifies options for the DeviceClass resources.
DeviceClasses *api.SelectorAllowedListSpec `json:"deviceClasses,omitempty"`
// Specifies options for the GatewayClass resources.
GatewayOptions GatewayOptions `json:"gatewayOptions,omitempty"`
// +optional
GatewayOptions GatewayOptions `json:"gatewayOptions,omitzero"`
// Toggling the Tenant resources cordoning, when enable resources cannot be deleted.
//+kubebuilder:default:=false
Cordoned bool `json:"cordoned,omitempty"`
@@ -110,11 +116,15 @@ func (p *Permissions) ListMatchingOwners(
// +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"`
metav1.ObjectMeta `json:"metadata,omitempty"`
metav1.TypeMeta `json:",inline"`
Spec TenantSpec `json:"spec,omitempty"`
Status TenantStatus `json:"status,omitempty"`
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec TenantSpec `json:"spec"`
// +optional
Status TenantStatus `json:"status,omitzero"`
}
func (in *Tenant) GetNamespaces() (res []string) {
@@ -130,7 +140,7 @@ func (in *Tenant) GetNamespaces() (res []string) {
// TenantList contains a list of Tenant.
type TenantList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []Tenant `json:"items"`
}

View File

@@ -13,7 +13,8 @@ type GlobalTenantResourceSpec struct {
TenantResourceSpec `json:",inline"`
// Defines the Tenant selector used target the tenants on which resources must be propagated.
TenantSelector metav1.LabelSelector `json:"tenantSelector,omitempty"`
// +optional
TenantSelector metav1.LabelSelector `json:"tenantSelector,omitzero"`
}
// GlobalTenantResourceStatus defines the observed state of GlobalTenantResource.
@@ -21,7 +22,7 @@ type GlobalTenantResourceStatus struct {
// List of Tenants addressed by the GlobalTenantResource.
SelectedTenants []string `json:"selectedTenants"`
// List of the replicated resources for the given TenantResource.
ProcessedItems ProcessedItems `json:"processedItems"`
ProcessedItems ProcessedItems `json:"processedItems,omitzero"`
}
type ProcessedItems []ObjectReferenceStatus
@@ -42,11 +43,15 @@ func (p *ProcessedItems) AsSet() sets.Set[string] {
// GlobalTenantResource allows to propagate resource replications to a specific subset of Tenant resources.
type GlobalTenantResource struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
metav1.TypeMeta `json:",inline"`
Spec GlobalTenantResourceSpec `json:"spec,omitempty"`
Status GlobalTenantResourceStatus `json:"status,omitempty"`
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec GlobalTenantResourceSpec `json:"spec"`
// +optional
Status GlobalTenantResourceStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
@@ -54,7 +59,7 @@ type GlobalTenantResource struct {
// GlobalTenantResourceList contains a list of GlobalTenantResource.
type GlobalTenantResourceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []GlobalTenantResource `json:"items"`
}

View File

@@ -56,11 +56,15 @@ type TenantResourceStatus struct {
// The object must be deployed in a Tenant Namespace, and cannot reference object living in non-Tenant namespaces.
// For such cases, the GlobalTenantResource must be used.
type TenantResource struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
metav1.TypeMeta `json:",inline"`
Spec TenantResourceSpec `json:"spec,omitempty"`
Status TenantResourceStatus `json:"status,omitempty"`
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
Spec TenantResourceSpec `json:"spec"`
// +optional
Status TenantResourceStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
@@ -68,7 +72,7 @@ type TenantResource struct {
// TenantResourceList contains a list of TenantResource.
type TenantResourceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []TenantResource `json:"items"`
}

View File

@@ -1,6 +1,6 @@
dependencies:
- name: capsule-proxy
repository: oci://ghcr.io/projectcapsule/charts
version: 0.9.13
digest: sha256:dbca86ef4afef07c79c0d5e049c6666a70d2b8990262224970f662531fffd9c3
generated: "2025-09-03T20:29:41.043265755Z"
version: 0.10.0
digest: sha256:b268fe0a87e4fa4d0196e5dac82c7e8ae20e96053f5ca860b1f7c44e3a357406
generated: "2025-12-09T15:58:45.796317945Z"

View File

@@ -6,7 +6,7 @@ home: https://github.com/projectcapsule/capsule
icon: https://github.com/projectcapsule/capsule/raw/main/assets/logo/capsule_small.png
dependencies:
- name: capsule-proxy
version: 0.9.13
version: 0.10.0
repository: "oci://ghcr.io/projectcapsule/charts"
condition: proxy.enabled
alias: proxy

View File

@@ -122,7 +122,7 @@ The following Values have changed key or Value:
| 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 | `"3"` | Set the log verbosity of the capsule with a value from 1 to 5 |
| manager.options.logLevel | string | `"info"` | Set the log verbosity of the capsule with a value from 1 to 5 |
| manager.options.nodeMetadata | object | `{"forbiddenAnnotations":{"denied":[],"deniedRegex":""},"forbiddenLabels":{"denied":[],"deniedRegex":""}}` | Allows to set the forbidden metadata for the worker nodes that could be patched by a Tenant |
| manager.options.protectedNamespaceRegex | string | `""` | If specified, disallows creation of namespaces matching the passed regexp |
| manager.options.userNames | list | `[]` | DEPRECATED: use users properties. Names of the users considered as Capsule users. |

View File

@@ -72,7 +72,7 @@ spec:
However ServiceAccounts which have been promoted to owner can not promote further serviceAccounts.
type: boolean
enableTLSReconciler:
default: true
default: false
description: |-
Toggles the TLS reconciler, the controller that is able to generate CA and certificates for the webhooks
when not using an already provided CA and certificate, or when these are managed externally with Vault, or cert-manager.
@@ -117,9 +117,6 @@ spec:
deniedRegex:
type: string
type: object
required:
- forbiddenAnnotations
- forbiddenLabels
type: object
overrides:
default:
@@ -198,6 +195,8 @@ spec:
required:
- enableTLSReconciler
type: object
required:
- spec
type: object
served: true
storage: true

View File

@@ -291,6 +291,8 @@ spec:
- processedItems
- selectedTenants
type: object
required:
- spec
type: object
served: true
storage: true

View File

@@ -151,6 +151,8 @@ spec:
type: string
type: object
type: object
required:
- spec
type: object
served: true
storage: true

View File

@@ -321,6 +321,8 @@ spec:
type: string
type: array
type: object
required:
- spec
type: object
served: true
storage: true

View File

@@ -239,6 +239,8 @@ spec:
required:
- processedItems
type: object
required:
- spec
type: object
served: true
storage: true

View File

@@ -1074,6 +1074,8 @@ spec:
- size
- state
type: object
required:
- spec
type: object
served: true
storage: false
@@ -2890,6 +2892,8 @@ spec:
- size
- state
type: object
required:
- spec
type: object
served: true
storage: true

View File

@@ -177,7 +177,7 @@ manager:
# -- Workers (MaxConcurrentReconciles) is the maximum number of concurrent Reconciles which can be run (ALPHA).
workers: 1
# -- Set the log verbosity of the capsule with a value from 1 to 5
logLevel: '3'
logLevel: "info"
# -- Define entities which are considered part of the Capsule construct.
# Users not mentioned here will be ignored by Capsule
users:

View File

@@ -91,15 +91,26 @@ var _ = Describe("creating namespace with status lifecycle", Label("namespace",
})
By("removing first namespace", func() {
Expect(k8sClient.Delete(context.TODO(), ns1)).Should(Succeed())
cs := impersonationClient(tnt.Spec.Owners[0].UserSpec.Name, withDefaultGroups(nil))
Expect(cs.Delete(context.TODO(), ns1)).Should(Succeed())
t := &capsulev1beta2.Tenant{}
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt.GetName()}, t)).Should(Succeed())
Eventually(func(g Gomega) {
t := &capsulev1beta2.Tenant{}
Expect(t.Status.Size).To(Equal(uint(1)))
err := k8sClient.Get(
context.TODO(),
types.NamespacedName{Name: tnt.GetName()},
t,
)
g.Expect(err).ToNot(HaveOccurred())
g.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")
instance := t.Status.GetInstance(&capsulev1beta2.TenantStatusNamespaceItem{
Name: ns1.GetName(),
UID: ns1.GetUID(),
})
g.Expect(instance).To(BeNil(), "Namespace instance should be nil")
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
By("removing second namespace", func() {

View File

@@ -104,9 +104,9 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label("
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())},
"owner": {client: impersonationClient(tnt.Spec.Owners[0].Name, withDefaultGroups(make([]string, 0))), matcher: Not(Succeed())},
"rb-user": {client: impersonationClient("bob", withDefaultGroups(make([]string, 0))), matcher: Not(Succeed())},
"rb-sa": {client: impersonationClient("system:serviceaccount:"+sa.GetNamespace()+":default", withDefaultGroups(make([]string, 0))), matcher: Not(Succeed())},
}
for name, tc := range personas {
@@ -197,9 +197,9 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label("
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()},
"rb-user": {client: impersonationClient("bob", withDefaultGroups(make([]string, 0))), matcher: Not(Succeed())},
"rb-sa": {client: impersonationClient("system:serviceaccount:"+sa.GetNamespace()+":default", withDefaultGroups(make([]string, 0))), matcher: Not(Succeed())},
"owner": {client: impersonationClient(tnt.Spec.Owners[0].Name, withDefaultGroups(make([]string, 0))), matcher: Succeed()},
}
for name, tc := range personas {
@@ -258,7 +258,7 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label("
client client.Client
matcher otypes.GomegaMatcher
}{
"owner": {client: impersonationClient(tnt.Spec.Owners[0].Name, withDefaultGroups(make([]string, 0)), k8sClient.Scheme()), matcher: Succeed()},
"owner": {client: impersonationClient(tnt.Spec.Owners[0].Name, withDefaultGroups(make([]string, 0))), matcher: Succeed()},
}
for name, tc := range personas {
@@ -294,14 +294,21 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label("
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()))
By("preventing the service account from deleting the namespace", func() {
newNs := NewNamespace("")
Expect(saClient.Create(context.TODO(), newNs)).To(Succeed())
Expect(saClient.Delete(context.TODO(), newNs)).To(Not(Succeed()))
TenantNamespaceList(tnt, defaultTimeoutInterval).
Should(ContainElements(ns.GetName(), newNs.GetName()))
Eventually(func(g Gomega) {
// Deletion should eventually be forbidden / fail
g.Expect(saClient.Delete(context.TODO(), newNs)).
ToNot(Succeed())
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
})
for name, tc := range personas {
By(fmt.Sprintf("trying to promote SA as %s", name))

View File

@@ -12,7 +12,6 @@ import (
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
@@ -115,16 +114,17 @@ func ownerClient(owner api.UserSpec) (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{
func impersonationClient(user string, groups []string) client.Client {
impersonatedCfg := rest.CopyConfig(cfg)
impersonatedCfg.Impersonate = rest.ImpersonationConfig{
UserName: user,
Groups: groups,
}
cl, err := client.New(c, client.Options{Scheme: scheme})
c, err := client.New(impersonatedCfg, client.Options{Scheme: k8sClient.Scheme()})
Expect(err).ToNot(HaveOccurred())
return cl
return c
}
func withDefaultGroups(groups []string) []string {

14
go.mod
View File

@@ -13,13 +13,13 @@ require (
github.com/valyala/fasttemplate v1.2.2
go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.27.1
golang.org/x/sync v0.18.0
k8s.io/api v0.34.2
k8s.io/apiextensions-apiserver v0.34.2
k8s.io/apimachinery v0.34.2
k8s.io/apiserver v0.34.2
k8s.io/client-go v0.34.2
k8s.io/dynamic-resource-allocation v0.34.2
golang.org/x/sync v0.19.0
k8s.io/api v0.34.3
k8s.io/apiextensions-apiserver v0.34.3
k8s.io/apimachinery v0.34.3
k8s.io/apiserver v0.34.3
k8s.io/client-go v0.34.3
k8s.io/dynamic-resource-allocation v0.34.3
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
sigs.k8s.io/cluster-api v1.11.3
sigs.k8s.io/controller-runtime v0.22.4

13
go.sum
View File

@@ -262,6 +262,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.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=
@@ -307,20 +309,31 @@ 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.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo=
k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE=
k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g=
k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0=
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE=
k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI=
k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo=
k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w=
k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
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.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ=
k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM=
k8s.io/dynamic-resource-allocation v0.34.2 h1:SjlRGSWl6CZXoJwQNL+Y0wRfdH8PkJ4mHRNK6MMj0bY=
k8s.io/dynamic-resource-allocation v0.34.2/go.mod h1:ul6I+gfrCmC+OCuVdN0/iykyB2sPrIqh2WyKQ3RQPCU=
k8s.io/dynamic-resource-allocation v0.34.3/go.mod h1:eYjQqNaHLfqXT94lbSXEy8ZLaUg1mGJ2JCEtNWM7e7M=
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-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=

View File

@@ -0,0 +1,14 @@
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: tenant-example
spec:
project: default
source:
path: gitops/simple/
repoURL: 'https://github.com/projectcapsule/examples.git'
targetRevision: HEAD
destination:
namespace: default
server: 'https://kubernetes.default.svc'

View File

@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: argocd
resources:
- application.yaml

View File

@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- release.flux.yaml

View File

@@ -0,0 +1,231 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: argocd
namespace: flux-system
spec:
serviceAccountName: kustomize-controller
interval: 30s
timeout: 10m
targetNamespace: argocd
releaseName: "argocd"
chart:
spec:
chart: argo-cd
version: "9.1.7"
sourceRef:
kind: HelmRepository
name: argocd
interval: 24h
install:
createNamespace: true
remediation:
retries: -1
upgrade:
remediation:
remediateLastFailure: true
retries: -1
driftDetection:
mode: enabled
values:
configs:
cm:
create: true
resource.customizations.health.capsule.clastix.io_Tenant: |
hs = {}
if obj.status ~= nil then
if obj.status.conditions ~= nil then
for i, condition in ipairs(obj.status.conditions) do
if condition.type == "Cordoned" and condition.status == "True" then
hs.status = "Suspended"
hs.message = condition.message
return hs
end
end
for i, condition in ipairs(obj.status.conditions) do
if condition.type == "Ready" and condition.status == "False" then
hs.status = "Degraded"
hs.message = condition.message
return hs
end
if condition.type == "Ready" and condition.status == "True" then
hs.status = "Healthy"
hs.message = condition.message
return hs
end
end
end
end
hs.status = "Progressing"
hs.message = "Waiting for Status"
return hs
resource.customizations.actions.capsule.clastix.io_Tenant: |
mergeBuiltinActions: true
discovery.lua: |
actions = {}
actions["cordon"] = {
["iconClass"] = "fa fa-solid fa-pause",
["disabled"] = true,
}
actions["uncordon"] = {
["iconClass"] = "fa fa-solid fa-play",
["disabled"] = true,
}
local suspend = false
if obj.spec ~= nil and obj.spec.cordoned ~= nil then
suspend = obj.spec.cordoned
end
if suspend then
actions["uncordon"]["disabled"] = false
else
actions["cordon"]["disabled"] = false
end
return actions
definitions:
- name: cordon
action.lua: |
if obj.spec == nil then
obj.spec = {}
end
obj.spec.cordoned = true
return obj
- name: uncordon
action.lua: |
if obj.spec ~= nil and obj.spec.cordoned ~= nil and obj.spec.cordoned then
obj.spec.cordoned = false
end
return obj
resource.customizations.health.Namespace: |
hs = {}
local function has_managed_ownerref()
if obj.metadata == nil or obj.metadata.ownerReferences == nil then
return false
end
for _, ref in ipairs(obj.metadata.ownerReferences) do
if ref.kind == "Tenant" and ref.apiVersion == "capsule.clastix.io/v1beta2" then
return true
end
end
return false
end
local labels = {}
if obj.metadata ~= nil and obj.metadata.labels ~= nil then
labels = obj.metadata.labels
end
local cordoned = labels["projectcapsule.dev/cordoned"] == "true"
if cordoned and has_managed_ownerref() then
hs.status = "Suspended"
hs.message = "Namespace is cordoned (tenant-managed)"
return hs
end
if obj.status ~= nil and obj.status.phase ~= nil then
if obj.status.phase == "Active" then
hs.status = "Healthy"
hs.message = "Namespace is Active"
return hs
else
hs.status = "Progressing"
hs.message = "Namespace phase is " .. obj.status.phase
return hs
end
end
hs.status = "Progressing"
hs.message = "Waiting for Namespace status"
return hs
resource.customizations.actions.Namespace: |
mergeBuiltinActions: true
discovery.lua: |
actions = {
cordon = {
iconClass = "fa fa-solid fa-pause",
disabled = true,
},
uncordon = {
iconClass = "fa fa-solid fa-play",
disabled = true,
},
}
local function has_managed_ownerref()
if obj.metadata == nil or obj.metadata.ownerReferences == nil then
return false
end
for _, ref in ipairs(obj.metadata.ownerReferences) do
if ref.kind == "Tenant" and ref.apiVersion == "capsule.clastix.io/v1beta2" then
return true
end
end
return false
end
if not has_managed_ownerref() then
return {}
end
local labels = {}
if obj.metadata ~= nil and obj.metadata.labels ~= nil then
labels = obj.metadata.labels
end
local cordoned = labels["projectcapsule.dev/cordoned"] == "true"
if cordoned then
actions["uncordon"].disabled = false
else
actions["cordon"].disabled = false
end
return actions
definitions:
- name: cordon
action.lua: |
if obj.metadata == nil then
obj.metadata = {}
end
if obj.metadata.labels == nil then
obj.metadata.labels = {}
end
obj.metadata.labels["projectcapsule.dev/cordoned"] = "true"
return obj
- name: uncordon
action.lua: |
if obj.metadata ~= nil and obj.metadata.labels ~= nil then
obj.metadata.labels["projectcapsule.dev/cordoned"] = "false"
end
return obj
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: argocd
namespace: flux-system
spec:
interval: 24h0m0s
url: https://argoproj.github.io/argo-helm

View File

@@ -7,9 +7,9 @@ import (
"context"
"errors"
"fmt"
"maps"
"sync"
"github.com/valyala/fasttemplate"
corev1 "k8s.io/api/core/v1"
apierr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -24,6 +24,7 @@ import (
ctrllog "sigs.k8s.io/controller-runtime/pkg/log"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
tpl "github.com/projectcapsule/capsule/pkg/template"
)
const (
@@ -40,13 +41,8 @@ func prepareAdditionalMetadata(m map[string]string) map[string]string {
return make(map[string]string)
}
// we need to create a new map to avoid modifying the original one
copied := make(map[string]string, len(m))
for k, v := range m {
copied[k] = v
}
return copied
// clone without mutating the original
return maps.Clone(m)
}
func (r *Processor) HandlePruning(ctx context.Context, current, desired sets.Set[string]) (updateStatus bool) {
@@ -247,14 +243,9 @@ func (r *Processor) HandleSection(ctx context.Context, tnt capsulev1beta2.Tenant
for rawIndex, item := range spec.RawItems {
template := string(item.Raw)
t := fasttemplate.New(template, "{{ ", " }}")
tmplString := tpl.TemplateForTenantAndNamespace(template, &tnt, &ns)
tmplString := t.ExecuteString(map[string]interface{}{
"tenant.name": tnt.Name,
"namespace": ns.Name,
})
obj, keysAndValues := unstructured.Unstructured{}, []interface{}{"index", rawIndex}
obj, keysAndValues := unstructured.Unstructured{}, []any{"index", rawIndex}
if _, _, decodeErr := codecFactory.UniversalDeserializer().Decode([]byte(tmplString), nil, &obj); decodeErr != nil {
log.Error(decodeErr, "unable to deserialize rawItem", keysAndValues...)
@@ -304,27 +295,18 @@ func (r *Processor) createOrUpdate(ctx context.Context, obj *unstructured.Unstru
rv := actual.GetResourceVersion()
actual.SetUnstructuredContent(desired.Object)
combinedLabels := obj.GetLabels()
if combinedLabels == nil {
combinedLabels = make(map[string]string)
}
for key, value := range labels {
combinedLabels[key] = value
}
combinedLabels := map[string]string{}
maps.Copy(combinedLabels, obj.GetLabels())
maps.Copy(combinedLabels, labels)
actual.SetLabels(combinedLabels)
combinedAnnotations := obj.GetAnnotations()
if combinedAnnotations == nil {
combinedAnnotations = make(map[string]string)
}
for key, value := range annotations {
combinedAnnotations[key] = value
}
combinedAnnotations := map[string]string{}
maps.Copy(combinedAnnotations, obj.GetAnnotations())
maps.Copy(combinedAnnotations, annotations)
actual.SetAnnotations(combinedAnnotations)
actual.SetResourceVersion(rv)
actual.SetUID(UID)

View File

@@ -6,6 +6,7 @@ package tenant
import (
"context"
"fmt"
"slices"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
@@ -155,13 +156,7 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.Controller
q workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
r.enqueueForTenantsWithCondition(ctx, e.Object, q, func(tnt *capsulev1beta2.Tenant, c client.Object) bool {
for _, n := range tnt.Status.Namespaces {
if n == c.GetNamespace() {
return true
}
}
return false
return slices.Contains(tnt.Status.Namespaces, c.GetNamespace())
})
},
UpdateFunc: func(
@@ -170,13 +165,7 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.Controller
q workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
r.enqueueForTenantsWithCondition(ctx, e.ObjectNew, q, func(tnt *capsulev1beta2.Tenant, c client.Object) bool {
for _, n := range tnt.Status.Namespaces {
if n == c.GetNamespace() {
return true
}
}
return false
return slices.Contains(tnt.Status.Namespaces, c.GetNamespace())
})
},
DeleteFunc: func(
@@ -241,6 +230,7 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.Controller
func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ctrl.Result, err error) {
r.Log = r.Log.WithValues("Request.Name", request.Name)
// Fetch the Tenant instance
instance := &capsulev1beta2.Tenant{}
if err = r.Get(ctx, request.NamespacedName, instance); err != nil {

View File

@@ -7,6 +7,7 @@ import (
"context"
"fmt"
"maps"
"slices"
"golang.org/x/sync/errgroup"
corev1 "k8s.io/api/core/v1"
@@ -213,7 +214,12 @@ func (r *Manager) collectNamespaces(ctx context.Context, tenant *capsulev1beta2.
return err
}
tenant.AssignNamespaces(list.Items)
// Drop namespaces that are currently being deleted (DeletionTimestamp != nil)
activeNamespaces := slices.DeleteFunc(list.Items, func(ns corev1.Namespace) bool {
return ns.DeletionTimestamp != nil
})
tenant.AssignNamespaces(activeNamespaces)
return err
}

View File

@@ -4,6 +4,8 @@
package utils
import (
"slices"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
@@ -98,12 +100,6 @@ func LabelsChanged(keys []string, oldLabels, newLabels map[string]string) bool {
func NamesMatchingPredicate(names ...string) builder.Predicates {
return builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
for _, name := range names {
if object.GetName() == name {
return true
}
}
return false
return slices.Contains(names, object.GetName())
}))
}

View File

@@ -30,7 +30,6 @@ type handler struct {
handlers []webhook.TypedHandler[*corev1.Namespace]
}
//nolint:dupl
func (h *handler) OnCreate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) webhook.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
userIsAdmin := users.IsAdminUser(req, h.cfg.Administrators())
@@ -63,35 +62,8 @@ func (h *handler) OnCreate(c client.Client, decoder admission.Decoder, recorder
}
}
//nolint:dupl
func (h *handler) OnDelete(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) webhook.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
userIsAdmin := users.IsAdminUser(req, h.cfg.Administrators())
if !userIsAdmin && !users.IsCapsuleUser(ctx, c, h.cfg, req.UserInfo.Username, req.UserInfo.Groups) {
return nil
}
ns := &corev1.Namespace{}
if err := decoder.Decode(req, ns); err != nil {
return utils.ErroredResponse(err)
}
tnt, err := tenant.GetTenantByLabels(ctx, c, ns)
if err != nil {
return utils.ErroredResponse(err)
}
if tnt == nil && userIsAdmin {
return nil
}
for _, hndl := range h.handlers {
if response := hndl.OnDelete(c, ns, decoder, recorder)(ctx, req); response != nil {
return response
}
}
return func(context.Context, admission.Request) *admission.Response {
return nil
}
}

View File

@@ -6,6 +6,7 @@ package mutation
import (
"context"
"encoding/json"
"maps"
"net/http"
corev1 "k8s.io/api/core/v1"
@@ -134,12 +135,10 @@ func mergeStringMap(dst, src map[string]string) map[string]string {
}
if dst == nil {
dst = make(map[string]string, len(src))
return maps.Clone(src)
}
for k, v := range src {
dst[k] = v
}
maps.Copy(dst, src)
return dst
}

View File

@@ -33,7 +33,6 @@ type handler struct {
handlers []webhook.TypedHandlerWithTenant[*corev1.Namespace]
}
//nolint:dupl
func (h *handler) OnCreate(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) webhook.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
userIsAdmin := users.IsAdminUser(req, h.cfg.Administrators())
@@ -66,7 +65,6 @@ func (h *handler) OnCreate(c client.Client, decoder admission.Decoder, recorder
}
}
//nolint:dupl
func (h *handler) OnDelete(c client.Client, decoder admission.Decoder, recorder record.EventRecorder) webhook.Func {
return func(ctx context.Context, req admission.Request) *admission.Response {
userIsAdmin := users.IsAdminUser(req, h.cfg.Administrators())
@@ -76,7 +74,7 @@ func (h *handler) OnDelete(c client.Client, decoder admission.Decoder, recorder
}
ns := &corev1.Namespace{}
if err := decoder.Decode(req, ns); err != nil {
if err := decoder.DecodeRaw(req.OldObject, ns); err != nil {
return utils.ErroredResponse(err)
}

View File

@@ -10,7 +10,9 @@ import (
// +kubebuilder:object:generate=true
type PoolExhaustionResource struct {
// Available Resources to be claimed
Available resource.Quantity `json:"available,omitempty"`
// +optional
Available resource.Quantity `json:"available,omitzero"`
// Requesting Resources
Requesting resource.Quantity `json:"requesting,omitempty"`
// +optional
Requesting resource.Quantity `json:"requesting,omitzero"`
}

View File

@@ -4,6 +4,7 @@
package api
import (
"slices"
"sort"
)
@@ -19,10 +20,8 @@ func (o OwnerListSpec) IsOwner(name string, groups []string) bool {
return true
}
case GroupOwner:
for _, group := range groups {
if group == owner.Name {
return true
}
if slices.Contains(groups, owner.Name) {
return true
}
}
}

View File

@@ -13,7 +13,9 @@ type ServiceOptions struct {
// Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means no IPs are allowed. Optional.
ExternalServiceIPs *ExternalServiceIPsSpec `json:"externalIPs,omitempty"`
// Define the labels that a Tenant Owner cannot set for their Service resources.
ForbiddenLabels ForbiddenListSpec `json:"forbiddenLabels,omitempty"`
// +optional
ForbiddenLabels ForbiddenListSpec `json:"forbiddenLabels,omitzero"`
// Define the annotations that a Tenant Owner cannot set for their Service resources.
ForbiddenAnnotations ForbiddenListSpec `json:"forbiddenAnnotations,omitempty"`
// +optional
ForbiddenAnnotations ForbiddenListSpec `json:"forbiddenAnnotations,omitzero"`
}

View File

@@ -4,6 +4,7 @@
package template
import (
"io"
"strings"
"github.com/valyala/fasttemplate"
@@ -12,19 +13,32 @@ import (
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
)
// TemplateForTenantAndNamespace applies templating to all values in the provided map in place.
func TemplateForTenantAndNamespace(m map[string]string, tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) {
for k, v := range m {
if !strings.Contains(v, "{{ ") && !strings.Contains(v, " }}") {
continue
// TemplateForTenantAndNamespace applies templatingto the provided string.
func TemplateForTenantAndNamespace(template string, tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) string {
if !strings.Contains(template, "{{") && !strings.Contains(template, "}}") {
return template
}
t := fasttemplate.New(template, "{{", "}}")
values := map[string]string{
"tenant.name": tnt.Name,
"namespace": ns.Name,
}
return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) {
key := strings.TrimSpace(tag)
if v, ok := values[key]; ok {
return w.Write([]byte(v))
}
t := fasttemplate.New(v, "{{ ", " }}")
tmplString := t.ExecuteString(map[string]interface{}{
"tenant.name": tnt.Name,
"namespace": ns.Name,
})
return 0, nil
})
}
m[k] = tmplString
// TemplateForTenantAndNamespace applies templating to all values in the provided map in place.
func TemplateForTenantAndNamespaceMap(m map[string]string, tnt *capsulev1beta2.Tenant, ns *corev1.Namespace) {
for k, v := range m {
m[k] = TemplateForTenantAndNamespace(v, tnt, ns)
}
}

View File

@@ -1,14 +1,16 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package template
package template_test
import (
"testing"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
tpl "github.com/projectcapsule/capsule/pkg/template"
)
func newTenant(name string) *capsulev1beta2.Tenant {
@@ -31,12 +33,90 @@ func TestTemplateForTenantAndNamespace_ReplacesPlaceholders(t *testing.T) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
got := tpl.TemplateForTenantAndNamespace(
"tenant={{tenant.name}}, ns={{namespace}}",
tnt,
ns,
)
want := "tenant=tenant-a, ns=ns-1"
if got != want {
t.Fatalf("expected %q, got %q", want, got)
}
}
func TestTemplateForTenantAndNamespace_ReplacesPlaceholdersSpaces(t *testing.T) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
got := tpl.TemplateForTenantAndNamespace(
"tenant={{ tenant.name }}, ns={{ namespace }}",
tnt,
ns,
)
want := "tenant=tenant-a, ns=ns-1"
if got != want {
t.Fatalf("expected %q, got %q", want, got)
}
}
func TestTemplateForTenantAndNamespace_OnlyTenant(t *testing.T) {
tnt := newTenant("tenant-x")
ns := newNamespace("ns-y")
got := tpl.TemplateForTenantAndNamespace("T={{tenant.name}}", tnt, ns)
want := "T=tenant-x"
if got != want {
t.Fatalf("expected %q, got %q", want, got)
}
}
func TestTemplateForTenantAndNamespace_OnlyNamespace(t *testing.T) {
tnt := newTenant("tenant-x")
ns := newNamespace("ns-y")
got := tpl.TemplateForTenantAndNamespace("N={{namespace}}", tnt, ns)
want := "N=ns-y"
if got != want {
t.Fatalf("expected %q, got %q", want, got)
}
}
func TestTemplateForTenantAndNamespace_NoDelimitersReturnsEmpty(t *testing.T) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
got := tpl.TemplateForTenantAndNamespace("plain-value-without-templates", tnt, ns)
if got != "plain-value-without-templates" {
t.Fatalf("expected empty string for input without delimiters, got %q", got)
}
}
func TestTemplateForTenantAndNamespace_UnknownKeyBecomesEmpty(t *testing.T) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
got := tpl.TemplateForTenantAndNamespace("X={{unknown.key}}", tnt, ns)
want := "X="
if got != want {
t.Fatalf("expected %q, got %q", want, got)
}
}
func TestTemplateForTenantAndNamespaceMap_ReplacesPlaceholders(t *testing.T) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
m := map[string]string{
"key1": "tenant={{ tenant.name }}, ns={{ namespace }}",
"key1": "tenant={{tenant.name}}, ns={{namespace}}",
"key2": "plain-value",
}
TemplateForTenantAndNamespace(m, tnt, ns)
tpl.TemplateForTenantAndNamespaceMap(m, tnt, ns)
if got := m["key1"]; got != "tenant=tenant-a, ns=ns-1" {
t.Fatalf("key1: expected %q, got %q", "tenant=tenant-a, ns=ns-1", got)
@@ -47,7 +127,27 @@ func TestTemplateForTenantAndNamespace_ReplacesPlaceholders(t *testing.T) {
}
}
func TestTemplateForTenantAndNamespace_SkipsValuesWithoutDelimiters(t *testing.T) {
func TestTemplateForTenantAndNamespaceMap_ReplacesPlaceholdersSpaces(t *testing.T) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
m := map[string]string{
"key1": "tenant={{ tenant.name }}, ns={{ namespace }}",
"key2": "plain-value",
}
tpl.TemplateForTenantAndNamespaceMap(m, tnt, ns)
if got := m["key1"]; got != "tenant=tenant-a, ns=ns-1" {
t.Fatalf("key1: expected %q, got %q", "tenant=tenant-a, ns=ns-1", got)
}
if got := m["key2"]; got != "plain-value" {
t.Fatalf("key2: expected %q to remain unchanged, got %q", "plain-value", got)
}
}
func TestTemplateForTenantAndNamespaceMap_SkipsValuesWithoutDelimiters(t *testing.T) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
@@ -57,20 +157,19 @@ func TestTemplateForTenantAndNamespace_SkipsValuesWithoutDelimiters(t *testing.T
"noTemplate2": "namespace {{namespace}}",
}
original1 := m["noTemplate1"]
original2 := m["noTemplate2"]
TemplateForTenantAndNamespace(m, tnt, ns)
tpl.TemplateForTenantAndNamespaceMap(m, tnt, ns)
if got := m["noTemplate1"]; got != original1 {
t.Fatalf("noTemplate1: expected %q to remain unchanged, got %q", original1, got)
if got := m["noTemplate1"]; got != "hello tenant-a" {
t.Fatalf("noTemplate1: expected %q, got %q", "tenant=tenant-a, ns=ns-1", got)
}
if got := m["noTemplate2"]; got != original2 {
if got := m["noTemplate2"]; got != "namespace ns-1" {
t.Fatalf("noTemplate2: expected %q to remain unchanged, got %q", original2, got)
}
}
func TestTemplateForTenantAndNamespace_MixedKeys(t *testing.T) {
func TestTemplateForTenantAndNamespaceMap_MixedKeys(t *testing.T) {
tnt := newTenant("tenant-x")
ns := newNamespace("ns-x")
@@ -80,7 +179,7 @@ func TestTemplateForTenantAndNamespace_MixedKeys(t *testing.T) {
"none": "static",
}
TemplateForTenantAndNamespace(m, tnt, ns)
tpl.TemplateForTenantAndNamespaceMap(m, tnt, ns)
if got := m["onlyTenant"]; got != "T=tenant-x" {
t.Fatalf("onlyTenant: expected %q, got %q", "T=tenant-x", got)
@@ -93,7 +192,7 @@ func TestTemplateForTenantAndNamespace_MixedKeys(t *testing.T) {
}
}
func TestTemplateForTenantAndNamespace_UnknownKeyBecomesEmpty(t *testing.T) {
func TestTemplateForTenantAndNamespaceMap_UnknownKeyBecomesEmpty(t *testing.T) {
tnt := newTenant("tenant-a")
ns := newNamespace("ns-1")
@@ -101,7 +200,7 @@ func TestTemplateForTenantAndNamespace_UnknownKeyBecomesEmpty(t *testing.T) {
"unknown": "X={{ unknown.key }}",
}
TemplateForTenantAndNamespace(m, tnt, ns)
tpl.TemplateForTenantAndNamespaceMap(m, tnt, ns)
// fasttemplate with missing key returns an empty string for that placeholder
if got := m["unknown"]; got != "X=" {

View File

@@ -59,8 +59,8 @@ func BuildNamespaceMetadataForTenant(ns *corev1.Namespace, tnt *capsulev1beta2.T
continue
}
template.TemplateForTenantAndNamespace(md.Labels, tnt, ns)
template.TemplateForTenantAndNamespace(md.Annotations, tnt, ns)
template.TemplateForTenantAndNamespaceMap(md.Labels, tnt, ns)
template.TemplateForTenantAndNamespaceMap(md.Annotations, tnt, ns)
utils.MapMergeNoOverrite(labels, md.Labels)
utils.MapMergeNoOverrite(annotations, md.Annotations)

View File

@@ -50,6 +50,7 @@ func IsCapsuleUser(
}
}
//nolint:modernize
for _, group := range cfg.UserGroups() {
if groupList.Find(group) {
if len(cfg.IgnoreUserWithGroups()) > 0 {