mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 04:26:45 +00:00
fix: metric recorders for all conditionals (#1998)
* 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> * feat: add metadata enforcement Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add metadata enforcement Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: wave of fixes Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: wave of fixes Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: wave of fixes Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: wave of fixes Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: wave of fixes Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: wave of fixes 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> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
8e1cc910bd
commit
4b07463705
@@ -186,4 +186,7 @@ type ObjectReferenceStatusCondition struct {
|
||||
|
||||
// Indicates wether the resource was created or adopted
|
||||
Created bool `json:"created,omitempty"`
|
||||
|
||||
// Indicates whether the referenced resource is cluster-scoped.
|
||||
ClusterScoped bool `json:"clusterScoped,omitempty"`
|
||||
}
|
||||
|
||||
+291
-165
@@ -13,6 +13,7 @@ import (
|
||||
k8smeta "k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -22,7 +23,6 @@ import (
|
||||
clt "github.com/projectcapsule/capsule/pkg/runtime/client"
|
||||
)
|
||||
|
||||
//nolint:gocognit
|
||||
func (p *Processor) Reconcile(
|
||||
ctx context.Context,
|
||||
log logr.Logger,
|
||||
@@ -31,177 +31,15 @@ func (p *Processor) Reconcile(
|
||||
acc Accumulator,
|
||||
opts ProcessorOptions,
|
||||
) (err error) {
|
||||
itemErrors := 0
|
||||
|
||||
log.V(5).Info("starting pruning items", "present", len(*processed))
|
||||
|
||||
failAndContinue := func(i meta.ObjectReferenceStatus, msg string, err error) bool { // replace ItemType
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
itemErrors++
|
||||
i.Status = metav1.ConditionFalse
|
||||
i.Message = msg + err.Error()
|
||||
processed.UpdateItem(i)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
terminatingNamespaces := map[string]bool{}
|
||||
|
||||
for _, i := range *processed {
|
||||
if _, exists := acc[i.GetKey("")]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
obj := &unstructured.Unstructured{}
|
||||
obj.SetGroupVersionKind(i.GetGVK())
|
||||
obj.SetName(i.GetName())
|
||||
|
||||
ns := i.GetNamespace()
|
||||
if ns != "" {
|
||||
obj.SetNamespace(ns)
|
||||
}
|
||||
|
||||
if i.LastApply.IsZero() {
|
||||
processed.RemoveItem(i)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if opts.Prune {
|
||||
log.V(4).Info("pruning resources", "Kind", i.Kind, "Name", i.Name, "Namespace", i.Namespace)
|
||||
|
||||
fieldOwner := opts.FieldOwnerPrefix + "/" + i.FieldOwner("")
|
||||
|
||||
deleted, reconErr := p.Prune(ctx, c, obj, fieldOwner, &i)
|
||||
if failAndContinue(i, "pruning failed for item: ", reconErr) {
|
||||
continue
|
||||
}
|
||||
|
||||
if deleted {
|
||||
processed.RemoveItem(i)
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Disown item (only when GET succeeded)
|
||||
patches, err := p.handleRemoveManagedMetadata(ctx, c, obj, opts.Owner)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
processed.RemoveItem(i)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if failAndContinue(i, "disowning failed for item: ", err) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:nestif
|
||||
if len(patches) > 0 {
|
||||
err = clt.ApplyPatches(ctx, c, obj, patches, meta.ResourceControllerFieldOwnerPrefix())
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
processed.RemoveItem(i)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if failAndContinue(i, "removing metdata failed for item: ", err) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processed.RemoveItem(i)
|
||||
}
|
||||
|
||||
if itemErrors > 0 {
|
||||
if itemErrors := p.pruneProcessedItems(ctx, log, c, processed, acc, opts); itemErrors > 0 {
|
||||
return fmt.Errorf("pruning of %d resources failed", itemErrors)
|
||||
}
|
||||
|
||||
log.V(5).Info("accumulation after pruning", "items", len(acc))
|
||||
|
||||
for _, item := range acc {
|
||||
or := meta.ObjectReferenceStatus{
|
||||
ResourceID: item.Resource,
|
||||
ObjectReferenceStatusCondition: meta.ObjectReferenceStatusCondition{
|
||||
Type: meta.ReadyCondition,
|
||||
},
|
||||
}
|
||||
|
||||
hadError := false
|
||||
|
||||
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,
|
||||
obj.Object,
|
||||
fieldOwner,
|
||||
opts.Force,
|
||||
opts.Adopt,
|
||||
opts.Owner,
|
||||
processed.GetItem(item.Resource),
|
||||
)
|
||||
|
||||
or.Created = created
|
||||
|
||||
if err != nil {
|
||||
hadError = true
|
||||
or.Status = metav1.ConditionFalse
|
||||
or.Message = "apply failed for item " + obj.Origin.Origin + ": " + err.Error()
|
||||
|
||||
log.V(4).Info("failed to apply item", "item", obj.Origin.Origin)
|
||||
} else {
|
||||
if ver != nil {
|
||||
or.LastApply = *ver
|
||||
}
|
||||
|
||||
or.Status = metav1.ConditionTrue
|
||||
|
||||
log.V(4).Info("successfully applied item", "item", obj.Origin.Origin, "version", ver)
|
||||
}
|
||||
|
||||
processed.UpdateItem(or)
|
||||
}
|
||||
|
||||
if hadError {
|
||||
itemErrors++
|
||||
}
|
||||
}
|
||||
|
||||
if itemErrors > 0 {
|
||||
if itemErrors := p.applyAccumulatedItems(ctx, log, c, processed, acc, opts); itemErrors > 0 {
|
||||
return fmt.Errorf("applying of %d resources failed", itemErrors)
|
||||
}
|
||||
|
||||
@@ -212,6 +50,285 @@ func (p *Processor) Reconcile(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) pruneProcessedItems(
|
||||
ctx context.Context,
|
||||
log logr.Logger,
|
||||
c client.Client,
|
||||
processed *meta.ProcessedItems,
|
||||
acc Accumulator,
|
||||
opts ProcessorOptions,
|
||||
) int {
|
||||
itemErrors := 0
|
||||
|
||||
for _, item := range *processed {
|
||||
if _, exists := acc[item.GetKey("")]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
if item.LastApply.IsZero() {
|
||||
processed.RemoveItem(item)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
obj, err := p.objectForProcessedItem(item)
|
||||
if failAndRecord(processed, &itemErrors, item, "resolving resource scope failed: ", err) {
|
||||
continue
|
||||
}
|
||||
|
||||
if p.pruneProcessedItem(ctx, log, c, processed, opts, item, obj, &itemErrors) {
|
||||
continue
|
||||
}
|
||||
|
||||
p.disownProcessedItem(ctx, c, processed, opts, item, obj, &itemErrors)
|
||||
}
|
||||
|
||||
return itemErrors
|
||||
}
|
||||
|
||||
func (p *Processor) pruneProcessedItem(
|
||||
ctx context.Context,
|
||||
log logr.Logger,
|
||||
c client.Client,
|
||||
processed *meta.ProcessedItems,
|
||||
opts ProcessorOptions,
|
||||
item meta.ObjectReferenceStatus,
|
||||
obj *unstructured.Unstructured,
|
||||
itemErrors *int,
|
||||
) bool {
|
||||
if !opts.Prune {
|
||||
return false
|
||||
}
|
||||
|
||||
log.V(4).Info("pruning resources", "Kind", item.Kind, "Name", item.Name, "Namespace", item.Namespace)
|
||||
|
||||
fieldOwner := opts.FieldOwnerPrefix + "/" + item.FieldOwner("")
|
||||
|
||||
deleted, err := p.Prune(ctx, c, obj, fieldOwner, &item)
|
||||
if failAndRecord(processed, itemErrors, item, "pruning failed for item: ", err) {
|
||||
return true
|
||||
}
|
||||
|
||||
if deleted {
|
||||
processed.RemoveItem(item)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Processor) disownProcessedItem(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
processed *meta.ProcessedItems,
|
||||
opts ProcessorOptions,
|
||||
item meta.ObjectReferenceStatus,
|
||||
obj *unstructured.Unstructured,
|
||||
itemErrors *int,
|
||||
) {
|
||||
patches, err := p.handleRemoveManagedMetadata(ctx, c, obj, opts.Owner)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
processed.RemoveItem(item)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if failAndRecord(processed, itemErrors, item, "disowning failed for item: ", err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:nestif
|
||||
if len(patches) > 0 {
|
||||
err = clt.ApplyPatches(ctx, c, obj, patches, meta.ResourceControllerFieldOwnerPrefix())
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
processed.RemoveItem(item)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if failAndRecord(processed, itemErrors, item, "removing metdata failed for item: ", err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processed.RemoveItem(item)
|
||||
}
|
||||
|
||||
func (p *Processor) applyAccumulatedItems(
|
||||
ctx context.Context,
|
||||
log logr.Logger,
|
||||
c client.Client,
|
||||
processed *meta.ProcessedItems,
|
||||
acc Accumulator,
|
||||
opts ProcessorOptions,
|
||||
) int {
|
||||
itemErrors := 0
|
||||
terminatingNamespaces := map[string]bool{}
|
||||
|
||||
for _, item := range acc {
|
||||
if p.applyAccumulatedItem(ctx, log, c, processed, item, opts, terminatingNamespaces) {
|
||||
itemErrors++
|
||||
}
|
||||
}
|
||||
|
||||
return itemErrors
|
||||
}
|
||||
|
||||
func (p *Processor) applyAccumulatedItem(
|
||||
ctx context.Context,
|
||||
log logr.Logger,
|
||||
c client.Client,
|
||||
processed *meta.ProcessedItems,
|
||||
item *AccumulatorItem,
|
||||
opts ProcessorOptions,
|
||||
terminatingNamespaces map[string]bool,
|
||||
) bool {
|
||||
or := meta.ObjectReferenceStatus{
|
||||
ResourceID: item.Resource,
|
||||
ObjectReferenceStatusCondition: meta.ObjectReferenceStatusCondition{
|
||||
Type: meta.ReadyCondition,
|
||||
},
|
||||
}
|
||||
|
||||
clusterScoped, err := p.isClusterScoped(item.Resource.GetGVK())
|
||||
if err != nil {
|
||||
or.Status = metav1.ConditionFalse
|
||||
or.Message = "resolving resource scope failed: " + err.Error()
|
||||
processed.UpdateItem(or)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
or.ClusterScoped = clusterScoped
|
||||
|
||||
hadError := false
|
||||
|
||||
for _, obj := range *item.Objects {
|
||||
if p.applyAccumulatorObject(ctx, log, c, processed, item, obj, opts, terminatingNamespaces, &or) {
|
||||
hadError = true
|
||||
}
|
||||
}
|
||||
|
||||
return hadError
|
||||
}
|
||||
|
||||
func (p *Processor) applyAccumulatorObject(
|
||||
ctx context.Context,
|
||||
log logr.Logger,
|
||||
c client.Client,
|
||||
processed *meta.ProcessedItems,
|
||||
item *AccumulatorItem,
|
||||
obj AccumulatorObject,
|
||||
opts ProcessorOptions,
|
||||
terminatingNamespaces map[string]bool,
|
||||
or *meta.ObjectReferenceStatus,
|
||||
) bool {
|
||||
fieldOwner := opts.FieldOwnerPrefix + "/" + item.Resource.FieldOwner("")
|
||||
|
||||
terminating, namespace, err := p.isNamespaceTerminatingForObject(ctx, obj.Object, terminatingNamespaces)
|
||||
if err != nil {
|
||||
or.Status = metav1.ConditionFalse
|
||||
or.Message = "checking namespace termination failed for item " + obj.Origin.Origin + ": " + err.Error()
|
||||
|
||||
processed.UpdateItem(*or)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
ver, created, err := p.Apply(
|
||||
ctx,
|
||||
c,
|
||||
obj.Object,
|
||||
fieldOwner,
|
||||
opts.Force,
|
||||
opts.Adopt,
|
||||
opts.Owner,
|
||||
processed.GetItem(item.Resource),
|
||||
)
|
||||
|
||||
or.Created = created
|
||||
|
||||
if err != nil {
|
||||
or.Status = metav1.ConditionFalse
|
||||
or.Message = "apply failed for item " + obj.Origin.Origin + ": " + err.Error()
|
||||
|
||||
log.V(4).Info("failed to apply item", "item", obj.Origin.Origin)
|
||||
} else {
|
||||
if ver != nil {
|
||||
or.LastApply = *ver
|
||||
}
|
||||
|
||||
or.Status = metav1.ConditionTrue
|
||||
|
||||
log.V(4).Info("successfully applied item", "item", obj.Origin.Origin, "version", ver)
|
||||
}
|
||||
|
||||
processed.UpdateItem(*or)
|
||||
|
||||
return err != nil
|
||||
}
|
||||
|
||||
func (p *Processor) objectForProcessedItem(item meta.ObjectReferenceStatus) (*unstructured.Unstructured, error) {
|
||||
obj := &unstructured.Unstructured{}
|
||||
obj.SetGroupVersionKind(item.GetGVK())
|
||||
obj.SetName(item.GetName())
|
||||
|
||||
clusterScoped := item.ClusterScoped
|
||||
if !clusterScoped {
|
||||
var err error
|
||||
|
||||
clusterScoped, err = p.isClusterScoped(item.GetGVK())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ns := item.GetNamespace()
|
||||
if ns != "" && !clusterScoped {
|
||||
obj.SetNamespace(ns)
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func failAndRecord(
|
||||
processed *meta.ProcessedItems,
|
||||
itemErrors *int,
|
||||
item meta.ObjectReferenceStatus,
|
||||
msg string,
|
||||
err error,
|
||||
) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
(*itemErrors)++
|
||||
item.Status = metav1.ConditionFalse
|
||||
item.Message = msg + err.Error()
|
||||
processed.UpdateItem(item)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Prune by reverting the patch by the given fieldOwner
|
||||
// If the item was created by the controller and has no more field-managers we are going to delete.
|
||||
func (r *Processor) Prune(
|
||||
@@ -282,6 +399,15 @@ func (r *Processor) Prune(
|
||||
return false, err
|
||||
}
|
||||
|
||||
func (r *Processor) isClusterScoped(gvk schema.GroupVersionKind) (bool, error) {
|
||||
mapping, err := r.Mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return mapping.Scope.Name() == k8smeta.RESTScopeNameRoot, nil
|
||||
}
|
||||
|
||||
// Completely prune the resource when there's no more managers and the resource was created by the controller.
|
||||
func (r *Processor) handlePruneDeletion(
|
||||
ctx context.Context,
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package processor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
k8smeta "k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/runtime/gvk"
|
||||
)
|
||||
|
||||
func TestObjectForProcessedItem(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mapper := k8smeta.NewDefaultRESTMapper([]schema.GroupVersion{{Version: "v1"}})
|
||||
mapper.Add(schema.GroupVersionKind{Version: "v1", Kind: "Secret"}, k8smeta.RESTScopeNamespace)
|
||||
mapper.Add(schema.GroupVersionKind{Version: "v1", Kind: "Namespace"}, k8smeta.RESTScopeRoot)
|
||||
|
||||
p := &Processor{Mapper: mapper}
|
||||
|
||||
t.Run("keeps namespace for namespaced resource", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
obj, err := p.objectForProcessedItem(meta.ObjectReferenceStatus{
|
||||
ResourceID: gvk.ResourceID{
|
||||
Version: "v1",
|
||||
Kind: "Secret",
|
||||
Namespace: "tenant-a",
|
||||
Name: "example",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if obj.GetNamespace() != "tenant-a" {
|
||||
t.Fatalf("expected namespace tenant-a, got %q", obj.GetNamespace())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("drops tracking namespace for mapped cluster scoped resource", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
obj, err := p.objectForProcessedItem(meta.ObjectReferenceStatus{
|
||||
ResourceID: gvk.ResourceID{
|
||||
Version: "v1",
|
||||
Kind: "Namespace",
|
||||
Namespace: "tenant-a",
|
||||
Name: "example",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if obj.GetNamespace() != "" {
|
||||
t.Fatalf("expected empty namespace, got %q", obj.GetNamespace())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("uses status flag without mapper lookup", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
obj, err := (&Processor{}).objectForProcessedItem(meta.ObjectReferenceStatus{
|
||||
ResourceID: gvk.ResourceID{
|
||||
Version: "v1",
|
||||
Kind: "UnknownClusterKind",
|
||||
Namespace: "tenant-a",
|
||||
Name: "example",
|
||||
},
|
||||
ObjectReferenceStatusCondition: meta.ObjectReferenceStatusCondition{
|
||||
ClusterScoped: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if obj.GetNamespace() != "" {
|
||||
t.Fatalf("expected empty namespace, got %q", obj.GetNamespace())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFailAndRecord(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
processed := meta.ProcessedItems{}
|
||||
itemErrors := 0
|
||||
item := meta.ObjectReferenceStatus{
|
||||
ResourceID: gvk.ResourceID{
|
||||
Version: "v1",
|
||||
Kind: "Secret",
|
||||
Name: "example",
|
||||
},
|
||||
ObjectReferenceStatusCondition: meta.ObjectReferenceStatusCondition{
|
||||
Status: metav1.ConditionTrue,
|
||||
},
|
||||
}
|
||||
|
||||
if failAndRecord(&processed, &itemErrors, item, "prefix: ", nil) {
|
||||
t.Fatal("expected nil error to be ignored")
|
||||
}
|
||||
|
||||
if itemErrors != 0 {
|
||||
t.Fatalf("expected no item errors, got %d", itemErrors)
|
||||
}
|
||||
|
||||
if failAndRecord(&processed, &itemErrors, item, "prefix: ", errors.New("boom")) != true {
|
||||
t.Fatal("expected error to be recorded")
|
||||
}
|
||||
|
||||
if itemErrors != 1 {
|
||||
t.Fatalf("expected one item error, got %d", itemErrors)
|
||||
}
|
||||
|
||||
got := processed.GetItem(item.ResourceID)
|
||||
if got == nil {
|
||||
t.Fatal("expected processed item to be recorded")
|
||||
}
|
||||
|
||||
if got.Status != metav1.ConditionFalse {
|
||||
t.Fatalf("expected status False, got %q", got.Status)
|
||||
}
|
||||
|
||||
if got.Message != "prefix: boom" {
|
||||
t.Fatalf("expected message %q, got %q", "prefix: boom", got.Message)
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ func (g GlobalCreatedItems) Func() client.IndexerFunc {
|
||||
|
||||
for _, pi := range tgr.Status.ProcessedItems {
|
||||
if pi.Created {
|
||||
out = append(out, pi.GetGVKKey(""))
|
||||
out = append(out, processedItemKey(pi))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ func (g GlobalProcessedItems) Func() client.IndexerFunc {
|
||||
|
||||
out := make([]string, 0, len(tgr.Status.ProcessedItems))
|
||||
for _, pi := range tgr.Status.ProcessedItems {
|
||||
out = append(out, pi.GetGVKKey(""))
|
||||
out = append(out, processedItemKey(pi))
|
||||
}
|
||||
|
||||
return out
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenantresource
|
||||
|
||||
import "github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
|
||||
func processedItemKey(item meta.ObjectReferenceStatus) string {
|
||||
ref := item.ResourceID
|
||||
if item.ClusterScoped {
|
||||
ref.Namespace = ""
|
||||
}
|
||||
|
||||
return ref.GetGVKKey("")
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenantresource
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
"github.com/projectcapsule/capsule/pkg/runtime/gvk"
|
||||
)
|
||||
|
||||
func TestCreatedItemsIndexersUseAdmissionKeyForClusterScopedItems(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
item := meta.ObjectReferenceStatus{
|
||||
ResourceID: gvk.ResourceID{
|
||||
Group: "rbac.authorization.k8s.io",
|
||||
Version: "v1",
|
||||
Kind: "ClusterRole",
|
||||
Namespace: "tenant-a",
|
||||
Name: "example",
|
||||
},
|
||||
ObjectReferenceStatusCondition: meta.ObjectReferenceStatusCondition{
|
||||
Created: true,
|
||||
ClusterScoped: true,
|
||||
},
|
||||
}
|
||||
|
||||
want := []string{
|
||||
gvk.ResourceID{
|
||||
Group: "rbac.authorization.k8s.io",
|
||||
Version: "v1",
|
||||
Kind: "ClusterRole",
|
||||
Name: "example",
|
||||
}.GetGVKKey(""),
|
||||
}
|
||||
|
||||
t.Run("global", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
idx := GlobalCreatedItems{}
|
||||
obj := &capsulev1beta2.GlobalTenantResource{}
|
||||
obj.Status.ProcessedItems = meta.ProcessedItems{item}
|
||||
|
||||
if got := idx.Func()(obj); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("unexpected keys\nwant=%#v\ngot =%#v", want, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("namespaced", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
idx := NamespacedCreatedItems{}
|
||||
obj := &capsulev1beta2.TenantResource{}
|
||||
obj.Status.ProcessedItems = meta.ProcessedItems{item}
|
||||
|
||||
if got := idx.Func()(obj); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("unexpected keys\nwant=%#v\ngot =%#v", want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcessedItemKeyKeepsNamespaceForNamespacedItems(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
item := meta.ObjectReferenceStatus{
|
||||
ResourceID: gvk.ResourceID{
|
||||
Version: "v1",
|
||||
Kind: "Secret",
|
||||
Namespace: "tenant-a",
|
||||
Name: "example",
|
||||
},
|
||||
}
|
||||
|
||||
want := item.GetGVKKey("")
|
||||
|
||||
if got := processedItemKey(item); got != want {
|
||||
t.Fatalf("unexpected key: want %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ func (g NamespacedCreatedItems) Func() client.IndexerFunc {
|
||||
|
||||
for _, pi := range tgr.Status.ProcessedItems {
|
||||
if pi.Created {
|
||||
out = append(out, pi.GetGVKKey(""))
|
||||
out = append(out, processedItemKey(pi))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ func (g NamespacedProcessedItems) Func() client.IndexerFunc {
|
||||
|
||||
out := make([]string, 0, len(tgr.Status.ProcessedItems))
|
||||
for _, pi := range tgr.Status.ProcessedItems {
|
||||
out = append(out, pi.GetGVKKey(""))
|
||||
out = append(out, processedItemKey(pi))
|
||||
}
|
||||
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user