mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-02-19 20:39:51 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd0675e8a3 | ||
|
|
e19575bcbd | ||
|
|
c06f54a3a3 | ||
|
|
cd5e2a82e1 | ||
|
|
2583215e8b | ||
|
|
8ceb375310 | ||
|
|
b0e086464d | ||
|
|
ad38a28468 | ||
|
|
f44b8b2b29 | ||
|
|
c832f56683 | ||
|
|
4b35b1e456 | ||
|
|
40cb5bdeeb |
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@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
|
||||
|
||||
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@149d184a5153ea45e6fbcef5588ac7b8c7af9835
|
||||
uses: github/codeql-action/upload-sarif@c43362b91a940600cde2ebae39ec7a35ad66bdc0
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
|
||||
2
.github/workflows/releaser.yml
vendored
2
.github/workflows/releaser.yml
vendored
@@ -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
|
||||
|
||||
11
Makefile
11
Makefile
@@ -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) || \
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -291,6 +291,8 @@ spec:
|
||||
- processedItems
|
||||
- selectedTenants
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
|
||||
@@ -151,6 +151,8 @@ spec:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
|
||||
@@ -321,6 +321,8 @@ spec:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
|
||||
@@ -239,6 +239,8 @@ spec:
|
||||
required:
|
||||
- processedItems
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
14
go.mod
@@ -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
13
go.sum
@@ -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=
|
||||
|
||||
14
hack/distro/argocd/application/application.yaml
Normal file
14
hack/distro/argocd/application/application.yaml
Normal 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'
|
||||
5
hack/distro/argocd/application/kustomization.yaml
Normal file
5
hack/distro/argocd/application/kustomization.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: argocd
|
||||
resources:
|
||||
- application.yaml
|
||||
4
hack/distro/argocd/kustomization.yaml
Normal file
4
hack/distro/argocd/kustomization.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- release.flux.yaml
|
||||
231
hack/distro/argocd/release.flux.yaml
Normal file
231
hack/distro/argocd/release.flux.yaml
Normal 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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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=" {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -50,6 +50,7 @@ func IsCapsuleUser(
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:modernize
|
||||
for _, group := range cfg.UserGroups() {
|
||||
if groupList.Find(group) {
|
||||
if len(cfg.IgnoreUserWithGroups()) > 0 {
|
||||
|
||||
Reference in New Issue
Block a user