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>
This commit is contained in:
Oliver Bähler
2025-12-10 18:34:42 +01:00
committed by GitHub
parent cd5e2a82e1
commit c06f54a3a3
34 changed files with 217 additions and 183 deletions

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}" \
@@ -415,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

@@ -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 {

View File

@@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
"maps"
"sync"
"github.com/valyala/fasttemplate"
@@ -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) {
@@ -249,12 +245,12 @@ func (r *Processor) HandleSection(ctx context.Context, tnt capsulev1beta2.Tenant
t := fasttemplate.New(template, "{{ ", " }}")
tmplString := t.ExecuteString(map[string]interface{}{
tmplString := t.ExecuteString(map[string]any{
"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 +300,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

@@ -20,7 +20,7 @@ func TemplateForTenantAndNamespace(m map[string]string, tnt *capsulev1beta2.Tena
}
t := fasttemplate.New(v, "{{ ", " }}")
tmplString := t.ExecuteString(map[string]interface{}{
tmplString := t.ExecuteString(map[string]any{
"tenant.name": tnt.Name,
"namespace": ns.Name,
})

View File

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