mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 12:36:39 +00:00
* feat: replicating resources upon namespace creation This feature enhances the (Global)TenantResource replication by triggering a replication of resources upon a Namespace creation: this speeds up the replication of resources without waiting for the resyncPeriod that could be delayed for several reasons. Signed-off-by: Dario Tranchitella <dario@tranchitella.eu> * fix: addressing fix from github copilot Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Dario Tranchitella <dario@tranchitella.eu> --------- Signed-off-by: Dario Tranchitella <dario@tranchitella.eu> Co-authored-by: Oliver Bähler <26610571+oliverbaehler@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
281 lines
6.9 KiB
Go
281 lines
6.9 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package processor
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/go-logr/logr"
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestProcessorScopeMatches(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
scope := Scope{Tenant: "tenant-a", Namespace: "ns-a"}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
id gvk.ResourceID
|
|
want bool
|
|
}{
|
|
{
|
|
name: "same tenant and namespace",
|
|
id: resourceID("tenant-a", "ns-a", "settings"),
|
|
want: true,
|
|
},
|
|
{
|
|
name: "other namespace",
|
|
id: resourceID("tenant-a", "ns-b", "settings"),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "other tenant",
|
|
id: resourceID("tenant-b", "ns-a", "settings"),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "no namespace",
|
|
id: resourceID("tenant-a", "", "settings"),
|
|
want: false,
|
|
},
|
|
} {
|
|
if got := scope.Matches(tc.id); got != tc.want {
|
|
t.Fatalf("%s: expected %v, got %v", tc.name, tc.want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScopedAccumulator(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
inScope := resourceID("tenant-a", "ns-a", "settings")
|
|
otherNs := resourceID("tenant-a", "ns-b", "settings")
|
|
otherTnt := resourceID("tenant-b", "ns-a", "settings")
|
|
|
|
acc := Accumulator{
|
|
inScope.GetKey(""): {Resource: inScope},
|
|
otherNs.GetKey(""): {Resource: otherNs},
|
|
otherTnt.GetKey(""): {Resource: otherTnt},
|
|
"empty": nil,
|
|
}
|
|
|
|
scoped, ignored := scopedAccumulator(acc, Scope{Tenant: "tenant-a", Namespace: "ns-a"})
|
|
|
|
if len(scoped) != 1 {
|
|
t.Fatalf("expected a single scoped item, got %d", len(scoped))
|
|
}
|
|
|
|
if scoped[inScope.GetKey("")] == nil {
|
|
t.Fatal("expected the in scope item to be retained")
|
|
}
|
|
|
|
if ignored != 2 {
|
|
t.Fatalf("expected 2 ignored items, got %d", ignored)
|
|
}
|
|
|
|
// The nil entry is dropped without being accounted as ignored.
|
|
if _, ok := scoped["empty"]; ok {
|
|
t.Fatal("expected the empty entry to be dropped")
|
|
}
|
|
}
|
|
|
|
func TestReconcileNamespaceRequiresNamespace(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
items, err := (&Processor{}).ReconcileNamespace(
|
|
context.Background(),
|
|
logr.Discard(),
|
|
nil,
|
|
nil,
|
|
Accumulator{},
|
|
ProcessorOptions{},
|
|
Scope{Tenant: "tenant-a"},
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected an error for a scope without a Namespace")
|
|
}
|
|
|
|
if items != nil {
|
|
t.Fatalf("expected no processed item, got %+v", items)
|
|
}
|
|
}
|
|
|
|
func TestReconcileNamespaceSeedsScopeOnly(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
inScope := resourceID("tenant-a", "ns-a", "settings")
|
|
otherNs := resourceID("tenant-a", "ns-b", "settings")
|
|
|
|
current := meta.ProcessedItems{
|
|
{ResourceID: otherNs, ObjectReferenceStatusCondition: meta.ObjectReferenceStatusCondition{Created: true}},
|
|
{ResourceID: inScope, ObjectReferenceStatusCondition: meta.ObjectReferenceStatusCondition{Created: true}},
|
|
}
|
|
|
|
// An empty Accumulator reaches out to no client at all: the outcome is only made
|
|
// of what was already tracked for the reconciled scope.
|
|
items, err := (&Processor{}).ReconcileNamespace(
|
|
context.Background(),
|
|
logr.Discard(),
|
|
nil,
|
|
current,
|
|
Accumulator{},
|
|
ProcessorOptions{},
|
|
Scope{Tenant: "tenant-a", Namespace: "ns-a"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
|
|
if len(items) != 1 || items[0].ResourceID != inScope {
|
|
t.Fatalf("expected only the in scope item, got %+v", items)
|
|
}
|
|
|
|
if !items[0].Created {
|
|
t.Fatal("expected the tracked creation flag to be carried over")
|
|
}
|
|
|
|
if len(current) != 2 {
|
|
t.Fatalf("expected the given items to be left untouched, got %+v", current)
|
|
}
|
|
}
|
|
|
|
func resourceID(tenant, namespace, name string) gvk.ResourceID {
|
|
return gvk.ResourceID{
|
|
TenantResourceIDWithOrigin: gvk.TenantResourceIDWithOrigin{
|
|
TenantResourceID: gvk.TenantResourceID{Tenant: tenant},
|
|
},
|
|
Version: "v1",
|
|
Kind: "ConfigMap",
|
|
Name: name,
|
|
Namespace: namespace,
|
|
}
|
|
}
|