mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-25 16:07:24 +00:00
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:
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
+1
-25
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user