fix: improve rules api (#1961)

* fix(controller): decode old object for delete requests

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

* chore: modernize golang

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

* chore: modernize golang

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

* chore: modernize golang

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

* fix: preserve ca-bundles injected from external providers

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: improve rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: improve rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: improve rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: improve rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: improve rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: improve rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: improve rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: improve rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
This commit is contained in:
Oliver Bähler
2026-06-10 00:18:40 +02:00
committed by GitHub
parent f4af5c8043
commit 34262c5536
47 changed files with 4271 additions and 980 deletions
+19
View File
@@ -5,6 +5,7 @@ package meta
import (
"context"
"maps"
"strings"
"time"
@@ -75,6 +76,24 @@ func TriggerRequestReconcileAnnotation(
})
}
func RemoveReconcileTriggerAnnotation(
obj client.Object,
) {
annotations := obj.GetAnnotations()
if _, ok := annotations[ReconcileAnnotation]; !ok {
return
}
annotations = maps.Clone(annotations)
delete(annotations, ReconcileAnnotation)
if len(annotations) == 0 {
obj.SetAnnotations(nil)
} else {
obj.SetAnnotations(annotations)
}
}
func annotationRemove(obj client.Object, anno string) {
annotations := obj.GetAnnotations()
+105
View File
@@ -48,6 +48,8 @@ func (p *Processor) Reconcile(
return true
}
terminatingNamespaces := map[string]bool{}
for _, i := range *processed {
if _, exists := acc[i.GetKey("")]; exists {
continue
@@ -137,6 +139,31 @@ func (p *Processor) Reconcile(
for _, obj := range *item.Objects {
fieldOwner := opts.FieldOwnerPrefix + "/" + item.Resource.FieldOwner("")
terminating, namespace, err := p.isNamespaceTerminatingForObject(ctx, obj.Object, terminatingNamespaces)
if err != nil {
hadError = true
or.Status = metav1.ConditionFalse
or.Message = "checking namespace termination failed for item " + obj.Origin.Origin + ": " + err.Error()
processed.UpdateItem(or)
continue
}
if terminating {
log.V(4).Info(
"skipping apply because namespace is terminating",
"item", obj.Origin.Origin,
"namespace", namespace,
"Kind", obj.Object.GetKind(),
"Name", obj.Object.GetName(),
)
processed.RemoveItem(or)
continue
}
ver, created, err := p.Apply(
ctx,
c,
@@ -458,3 +485,81 @@ func (r *Processor) handleCreatedMetadata(
existingObject.GetName(),
)
}
func (r *Processor) isNamespaceTerminatingForObject(
ctx context.Context,
obj *unstructured.Unstructured,
cache map[string]bool,
) (terminating bool, namespace string, err error) {
// The Namespace object itself is cluster-scoped, but if Capsule is applying
// a Namespace which is already terminating, we should skip it as well.
if obj.GroupVersionKind().Group == "" && obj.GetKind() == "Namespace" {
namespace = obj.GetName()
ns := &corev1.Namespace{}
if err := r.GatherClient.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil {
if apierrors.IsNotFound(err) {
cache[namespace] = false
return false, namespace, nil
}
return false, namespace, err
}
terminating = ns.DeletionTimestamp != nil || ns.Status.Phase == corev1.NamespaceTerminating
cache[namespace] = terminating
return terminating, namespace, nil
}
mapping, err := r.Mapper.RESTMapping(
obj.GroupVersionKind().GroupKind(),
obj.GroupVersionKind().Version,
)
if err != nil {
return false, "", err
}
if mapping.Scope.Name() != k8smeta.RESTScopeNameNamespace {
return false, "", nil
}
namespace = obj.GetNamespace()
if namespace == "" {
return false, "", nil
}
return r.isNamespaceTerminating(ctx, namespace, cache)
}
func (r *Processor) isNamespaceTerminating(
ctx context.Context,
namespace string,
cache map[string]bool,
) (bool, string, error) {
if namespace == "" {
return false, namespace, nil
}
if terminating, ok := cache[namespace]; ok {
return terminating, namespace, nil
}
ns := &corev1.Namespace{}
if err := r.GatherClient.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil {
if apierrors.IsNotFound(err) {
cache[namespace] = true
return true, namespace, nil
}
return false, namespace, err
}
terminating := ns.DeletionTimestamp != nil || ns.Status.Phase == corev1.NamespaceTerminating
cache[namespace] = terminating
return terminating, namespace, nil
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package rules
import "slices"
func (a ActionType) OrDefault() ActionType {
if a == "" {
return ActionTypeDeny
}
return a
}
func (e NamespaceRuleEnforceBody) GetWorkloadTargets(target WorkloadValidationTarget) bool {
if len(e.Workloads.Targets) == 0 {
return true
}
return slices.Contains(e.Workloads.Targets, target)
}
func (e NamespaceRuleEnforceBody) WorkloadTargetsAny(targets ...WorkloadValidationTarget) bool {
if len(e.Workloads.Targets) == 0 {
return true
}
return slices.ContainsFunc(targets, e.GetWorkloadTargets)
}
+2 -3
View File
@@ -12,7 +12,6 @@ type NamespaceRuleEnforceBody struct {
//+kubebuilder:default:=deny
Action ActionType `json:"action,omitempty"`
// Define registries which are allowed to be used within this tenant
// The rules are aggregated, since you can use Regular Expressions the match registry endpoints
Registries []OCIRegistry `json:"registries,omitempty"`
// Enforcement for Workloads (Pods)
Workloads NamespaceRuleEnforceWorkloadsBody `json:"workloads,omitempty"`
}
@@ -16,40 +16,16 @@ func (i ImagePullPolicySpec) String() string {
return string(i)
}
// +kubebuilder:validation:Enum=pod/images;pod/volumes
type RegistryValidationTarget string
const (
ValidateImages RegistryValidationTarget = "pod/images"
ValidateVolumes RegistryValidationTarget = "pod/volumes"
)
// +kubebuilder:object:generate=true
type OCIRegistry struct {
api.RegExpression `json:",inline"`
// Deprecated: Use exp field
//
// OCI Registry endpoint, is treated as regular expression.
Registry string `json:"url,omitempty"`
// Allowed PullPolicy for the given registry. Supplying no value allows all policies.
// +optional
// +kubebuilder:validation:Items:Enum=Always;Never;IfNotPresent
Policy []corev1.PullPolicy `json:"policy,omitempty"`
// Requesting Resources
//+kubebuilder:default:={pod/images,pod/volumes}
Validation []RegistryValidationTarget `json:"validation,omitempty"`
}
func (r OCIRegistry) Expression() api.RegExpression {
if r.RegExpression.Expression != "" {
return r.RegExpression
}
return api.RegExpression{
Expression: r.Registry,
Negate: false,
}
return r.RegExpression
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package rules
import corev1 "k8s.io/api/core/v1"
// +kubebuilder:validation:Enum=pod/initcontainers;pod/ephemeralcontainers;pod/containers;pod/volumes
type WorkloadValidationTarget string
const (
DeprecatedValidateImages WorkloadValidationTarget = "pod/images"
ValidateInitContainers WorkloadValidationTarget = "pod/initcontainers"
ValidateEphemeralContainers WorkloadValidationTarget = "pod/ephemeralcontainers"
ValidateContainers WorkloadValidationTarget = "pod/containers"
ValidateVolumes WorkloadValidationTarget = "pod/volumes"
)
// +kubebuilder:object:generate=true
type NamespaceRuleEnforceWorkloadsBody struct {
// Define the enforcement targets this rule applies to.
// If empty, each webhook applies its own backwards-compatible default.
// +optional
Targets []WorkloadValidationTarget `json:"targets,omitempty"`
// Define Pod QoS classes matched by this enforcement rule.
// Supported values are Guaranteed, Burstable and BestEffort.
// +optional
QoSClasses []corev1.PodQOSClass `json:"qosClasses,omitempty"`
// Define registries which are allowed to be used within this tenant
// The rules are aggregated, since you can use Regular Expressions the match registry endpoints
Registries []OCIRegistry `json:"registries,omitempty"`
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
type NamespaceRulePermissionBody struct {
// Define Promotion Rules which distributed additional ClusterRoles across the Tenant
// for promoted ServiceAccounts.
Promotions []*NamespaceRulePromotionRule `json:"rules,omitempty"`
Promotions []*NamespaceRulePromotionRule `json:"promotions,omitempty"`
}
// +kubebuilder:object:generate=true
+3 -3
View File
@@ -12,18 +12,18 @@ import (
type NamespaceRuleBodyNamespace struct {
// Enforcement for given rule
//+optional
Enforce NamespaceRuleEnforceBody `json:"enforce,omitzero"`
Enforce *NamespaceRuleEnforceBody `json:"enforce,omitzero"`
}
// Rules Distributed via Tenants
// +kubebuilder:object:generate=true
type NamespaceRuleBodyTenant struct {
NamespaceRuleBodyNamespace `json:",inline"`
*NamespaceRuleBodyNamespace `json:",inline"`
// Select namespaces which are going to be targeted with this rule
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
// Permissions for given rule
//+optional
Permissions NamespaceRulePermissionBody `json:"permissions,omitzero"`
Permissions NamespaceRulePermissionBody `json:"permissions,omitempty"`
}
+39 -10
View File
@@ -15,7 +15,11 @@ import (
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRuleBodyNamespace) DeepCopyInto(out *NamespaceRuleBodyNamespace) {
*out = *in
in.Enforce.DeepCopyInto(&out.Enforce)
if in.Enforce != nil {
in, out := &in.Enforce, &out.Enforce
*out = new(NamespaceRuleEnforceBody)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRuleBodyNamespace.
@@ -31,7 +35,11 @@ func (in *NamespaceRuleBodyNamespace) DeepCopy() *NamespaceRuleBodyNamespace {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRuleBodyTenant) DeepCopyInto(out *NamespaceRuleBodyTenant) {
*out = *in
in.NamespaceRuleBodyNamespace.DeepCopyInto(&out.NamespaceRuleBodyNamespace)
if in.NamespaceRuleBodyNamespace != nil {
in, out := &in.NamespaceRuleBodyNamespace, &out.NamespaceRuleBodyNamespace
*out = new(NamespaceRuleBodyNamespace)
(*in).DeepCopyInto(*out)
}
if in.NamespaceSelector != nil {
in, out := &in.NamespaceSelector, &out.NamespaceSelector
*out = new(metav1.LabelSelector)
@@ -53,6 +61,32 @@ func (in *NamespaceRuleBodyTenant) DeepCopy() *NamespaceRuleBodyTenant {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRuleEnforceBody) DeepCopyInto(out *NamespaceRuleEnforceBody) {
*out = *in
in.Workloads.DeepCopyInto(&out.Workloads)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRuleEnforceBody.
func (in *NamespaceRuleEnforceBody) DeepCopy() *NamespaceRuleEnforceBody {
if in == nil {
return nil
}
out := new(NamespaceRuleEnforceBody)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NamespaceRuleEnforceWorkloadsBody) DeepCopyInto(out *NamespaceRuleEnforceWorkloadsBody) {
*out = *in
if in.Targets != nil {
in, out := &in.Targets, &out.Targets
*out = make([]WorkloadValidationTarget, len(*in))
copy(*out, *in)
}
if in.QoSClasses != nil {
in, out := &in.QoSClasses, &out.QoSClasses
*out = make([]v1.PodQOSClass, len(*in))
copy(*out, *in)
}
if in.Registries != nil {
in, out := &in.Registries, &out.Registries
*out = make([]OCIRegistry, len(*in))
@@ -62,12 +96,12 @@ func (in *NamespaceRuleEnforceBody) DeepCopyInto(out *NamespaceRuleEnforceBody)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRuleEnforceBody.
func (in *NamespaceRuleEnforceBody) DeepCopy() *NamespaceRuleEnforceBody {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceRuleEnforceWorkloadsBody.
func (in *NamespaceRuleEnforceWorkloadsBody) DeepCopy() *NamespaceRuleEnforceWorkloadsBody {
if in == nil {
return nil
}
out := new(NamespaceRuleEnforceBody)
out := new(NamespaceRuleEnforceWorkloadsBody)
in.DeepCopyInto(out)
return out
}
@@ -132,11 +166,6 @@ func (in *OCIRegistry) DeepCopyInto(out *OCIRegistry) {
*out = make([]v1.PullPolicy, len(*in))
copy(*out, *in)
}
if in.Validation != nil {
in, out := &in.Validation, &out.Validation
*out = make([]RegistryValidationTarget, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIRegistry.