mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-20 04:56:44 +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>
278 lines
9.6 KiB
Go
278 lines
9.6 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package meta_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"github.com/projectcapsule/capsule/pkg/api/meta"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/gvk"
|
|
)
|
|
|
|
func mkItem(tenant, namespace, name, kind string, status metav1.ConditionStatus, condType, msg string, created bool, lastApply metav1.Time) meta.ObjectReferenceStatus {
|
|
return meta.ObjectReferenceStatus{
|
|
ResourceID: gvk.ResourceID{
|
|
TenantResourceIDWithOrigin: gvk.TenantResourceIDWithOrigin{
|
|
TenantResourceID: gvk.TenantResourceID{Tenant: tenant},
|
|
Origin: "",
|
|
},
|
|
Group: "",
|
|
Version: "",
|
|
Kind: kind,
|
|
Name: name,
|
|
Namespace: namespace,
|
|
},
|
|
ObjectReferenceStatusCondition: meta.ObjectReferenceStatusCondition{
|
|
Status: status,
|
|
Type: condType,
|
|
Message: msg,
|
|
LastApply: lastApply,
|
|
Created: created,
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestProcessedItems_UpdateItem_AppendsNew(t *testing.T) {
|
|
var p meta.ProcessedItems
|
|
|
|
now := metav1.NewTime(time.Now())
|
|
|
|
item := mkItem("t1", "ns1", "name1", "Secret", metav1.ConditionTrue, "Ready", "ok", true, now)
|
|
p.UpdateItem(item)
|
|
|
|
if len(p) != 1 {
|
|
t.Fatalf("expected 1 item, got %d", len(p))
|
|
}
|
|
if p[0].ResourceID != item.ResourceID {
|
|
t.Fatalf("expected ResourceID %+v, got %+v", item.ResourceID, p[0].ResourceID)
|
|
}
|
|
if p[0].ObjectReferenceStatusCondition != item.ObjectReferenceStatusCondition {
|
|
t.Fatalf("expected condition %+v, got %+v", item.ObjectReferenceStatusCondition, p[0].ObjectReferenceStatusCondition)
|
|
}
|
|
}
|
|
|
|
func TestProcessedItems_UpdateItem_UpdatesExistingWithoutDuplicate(t *testing.T) {
|
|
now1 := metav1.NewTime(time.Now().Add(-1 * time.Hour))
|
|
now2 := metav1.NewTime(time.Now())
|
|
|
|
original := mkItem("t1", "ns1", "name1", "Secret", metav1.ConditionFalse, "Ready", "initial", false, now1)
|
|
updated := mkItem("t1", "ns1", "name1", "Secret", metav1.ConditionTrue, "Ready", "updated", true, now2)
|
|
|
|
p := meta.ProcessedItems{original}
|
|
p.UpdateItem(updated)
|
|
|
|
if len(p) != 1 {
|
|
t.Fatalf("expected 1 item (updated in place), got %d", len(p))
|
|
}
|
|
|
|
// Resource identity must remain same
|
|
if p[0].ResourceID != original.ResourceID {
|
|
t.Fatalf("expected ResourceID unchanged %+v, got %+v", original.ResourceID, p[0].ResourceID)
|
|
}
|
|
|
|
// Condition should be replaced
|
|
if p[0].ObjectReferenceStatusCondition != updated.ObjectReferenceStatusCondition {
|
|
t.Fatalf("expected condition replaced with %+v, got %+v", updated.ObjectReferenceStatusCondition, p[0].ObjectReferenceStatusCondition)
|
|
}
|
|
}
|
|
|
|
func TestProcessedItems_RemoveItem_RemovesMatchingByResourceID(t *testing.T) {
|
|
now := metav1.NewTime(time.Now())
|
|
|
|
a := mkItem("t1", "ns1", "name1", "Secret", metav1.ConditionTrue, "Ready", "a", true, now)
|
|
b := mkItem("t1", "ns1", "name2", "ConfigMap", metav1.ConditionTrue, "Ready", "b", true, now)
|
|
c := mkItem("t2", "ns9", "name9", "Secret", metav1.ConditionFalse, "Ready", "c", false, now)
|
|
|
|
p := meta.ProcessedItems{a, b, c}
|
|
|
|
// Remove b
|
|
p.RemoveItem(b)
|
|
|
|
if len(p) != 2 {
|
|
t.Fatalf("expected 2 items after removal, got %d", len(p))
|
|
}
|
|
|
|
// Ensure b is gone, others remain
|
|
for _, it := range p {
|
|
if it.ResourceID == b.ResourceID {
|
|
t.Fatalf("did not expect removed item %+v to remain", b.ResourceID)
|
|
}
|
|
}
|
|
// Ensure a and c still exist
|
|
foundA, foundC := false, false
|
|
for _, it := range p {
|
|
if it.ResourceID == a.ResourceID {
|
|
foundA = true
|
|
}
|
|
if it.ResourceID == c.ResourceID {
|
|
foundC = true
|
|
}
|
|
}
|
|
if !foundA || !foundC {
|
|
t.Fatalf("expected remaining items to include a=%v c=%v", foundA, foundC)
|
|
}
|
|
}
|
|
|
|
func TestProcessedItems_GetItem_ReturnsPointerToSliceElement(t *testing.T) {
|
|
now := metav1.NewTime(time.Now())
|
|
|
|
a := mkItem("t1", "ns1", "name1", "Secret", metav1.ConditionFalse, "Ready", "a", false, now)
|
|
b := mkItem("t1", "ns1", "name2", "Secret", metav1.ConditionFalse, "Ready", "b", false, now)
|
|
|
|
p := meta.ProcessedItems{a, b}
|
|
|
|
ptr := p.GetItem(b.ResourceID)
|
|
if ptr == nil {
|
|
t.Fatalf("expected to find item %v", b.ResourceID)
|
|
}
|
|
|
|
// Mutate through pointer and ensure slice reflects it (proves it's not a copy)
|
|
ptr.Message = "changed"
|
|
ptr.Status = metav1.ConditionTrue
|
|
ptr.Created = true
|
|
|
|
if p[1].Message != "changed" {
|
|
t.Fatalf("expected slice element Message to be changed, got %q", p[1].Message)
|
|
}
|
|
if p[1].Status != metav1.ConditionTrue {
|
|
t.Fatalf("expected slice element Status to be changed, got %q", p[1].Status)
|
|
}
|
|
if p[1].Created != true {
|
|
t.Fatalf("expected slice element Created to be changed, got %v", p[1].Created)
|
|
}
|
|
|
|
// Not found case
|
|
none := p.GetItem(gvk.ResourceID{Name: "does-not-exist"})
|
|
if none != nil {
|
|
t.Fatalf("expected nil for non-existent item, got %+v", none)
|
|
}
|
|
}
|
|
|
|
func TestProcessedItems_SortDeterministic(t *testing.T) {
|
|
now := metav1.NewTime(time.Now())
|
|
|
|
// Intentionally shuffled order
|
|
i1 := mkItem("tenant-b", "ns-a", "name-a", "Secret", metav1.ConditionTrue, "Ready", "", true, now)
|
|
i2 := mkItem("tenant-a", "ns-b", "name-a", "Secret", metav1.ConditionTrue, "Ready", "", true, now)
|
|
i3 := mkItem("tenant-a", "ns-a", "name-b", "Secret", metav1.ConditionTrue, "Ready", "", true, now)
|
|
i4 := mkItem("tenant-a", "ns-a", "name-a", "ZKind", metav1.ConditionTrue, "Ready", "", true, now)
|
|
i5 := mkItem("tenant-a", "ns-a", "name-a", "AKind", metav1.ConditionTrue, "Ready", "", true, now)
|
|
i6 := mkItem("tenant-b", "ns-a", "name-a", "ConfigMap", metav1.ConditionTrue, "Ready", "", true, now)
|
|
|
|
p := meta.ProcessedItems{i1, i2, i3, i4, i5, i6}
|
|
p.SortDeterministic()
|
|
|
|
// Expected order by: Tenant, Namespace, Name, Kind
|
|
want := []gvk.ResourceID{
|
|
i5.ResourceID, // tenant-a, ns-a, name-a, AKind
|
|
i4.ResourceID, // tenant-a, ns-a, name-a, ZKind
|
|
i3.ResourceID, // tenant-a, ns-a, name-b, Secret
|
|
i2.ResourceID, // tenant-a, ns-b, name-a, Secret
|
|
i6.ResourceID, // tenant-b, ns-a, name-a, ConfigMap
|
|
i1.ResourceID, // tenant-b, ns-a, name-a, Secret
|
|
}
|
|
|
|
if len(p) != len(want) {
|
|
t.Fatalf("expected %d items, got %d", len(want), len(p))
|
|
}
|
|
|
|
for idx := range want {
|
|
if p[idx].ResourceID != want[idx] {
|
|
t.Fatalf("at index %d: expected %v, got %v", idx, want[idx], p[idx].ResourceID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProcessedItems_InScope(t *testing.T) {
|
|
now := metav1.NewTime(time.Now())
|
|
|
|
inA := mkItem("tenant-a", "ns-a", "name-a", "Secret", metav1.ConditionTrue, "Ready", "", true, now)
|
|
inB := mkItem("tenant-a", "ns-a", "name-b", "ConfigMap", metav1.ConditionTrue, "Ready", "", true, now)
|
|
otherNs := mkItem("tenant-a", "ns-b", "name-a", "Secret", metav1.ConditionTrue, "Ready", "", true, now)
|
|
otherTnt := mkItem("tenant-b", "ns-a", "name-a", "Secret", metav1.ConditionTrue, "Ready", "", true, now)
|
|
clusterWide := mkItem("tenant-a", "", "name-a", "ClusterRole", metav1.ConditionTrue, "Ready", "", true, now)
|
|
|
|
p := meta.ProcessedItems{inA, otherNs, inB, otherTnt, clusterWide}
|
|
|
|
scoped := p.InScope("tenant-a", "ns-a")
|
|
|
|
if len(scoped) != 2 {
|
|
t.Fatalf("expected 2 scoped items, got %d", len(scoped))
|
|
}
|
|
|
|
if scoped[0].ResourceID != inA.ResourceID || scoped[1].ResourceID != inB.ResourceID {
|
|
t.Fatalf("expected the scoped items in their original order, got %+v", scoped)
|
|
}
|
|
|
|
// The receiver must not be affected by any mutation of the returned copy.
|
|
updated := inA
|
|
updated.Message = "mutated"
|
|
scoped.UpdateItem(updated)
|
|
|
|
if p[0].Message != "" {
|
|
t.Fatalf("expected the source item to be untouched, got message %q", p[0].Message)
|
|
}
|
|
}
|
|
|
|
func TestProcessedItems_ReplaceScope(t *testing.T) {
|
|
now := metav1.NewTime(time.Now())
|
|
|
|
tracked := mkItem("tenant-a", "ns-a", "name-a", "Secret", metav1.ConditionFalse, "Ready", "failed", true, now)
|
|
gone := mkItem("tenant-a", "ns-a", "name-gone", "Secret", metav1.ConditionTrue, "Ready", "", true, now)
|
|
otherNs := mkItem("tenant-a", "ns-b", "name-a", "Secret", metav1.ConditionTrue, "Ready", "keep", true, now)
|
|
otherTnt := mkItem("tenant-b", "ns-a", "name-a", "Secret", metav1.ConditionTrue, "Ready", "keep", true, now)
|
|
|
|
p := meta.ProcessedItems{tracked, otherNs, gone, otherTnt}
|
|
|
|
reapplied := mkItem("tenant-a", "ns-a", "name-a", "Secret", metav1.ConditionTrue, "Ready", "applied", true, now)
|
|
fresh := mkItem("tenant-a", "ns-a", "name-new", "ConfigMap", metav1.ConditionTrue, "Ready", "", false, now)
|
|
// Out of the replaced scope: it must not leak into the result.
|
|
foreign := mkItem("tenant-a", "ns-c", "name-a", "Secret", metav1.ConditionTrue, "Ready", "", true, now)
|
|
|
|
p.ReplaceScope("tenant-a", "ns-a", meta.ProcessedItems{reapplied, fresh, foreign})
|
|
|
|
want := []gvk.ResourceID{
|
|
reapplied.ResourceID, // updated in place, position preserved
|
|
otherNs.ResourceID, // untouched
|
|
otherTnt.ResourceID, // untouched, shifted by the dropped item
|
|
fresh.ResourceID, // appended
|
|
}
|
|
|
|
if len(p) != len(want) {
|
|
t.Fatalf("expected %d items, got %d: %+v", len(want), len(p), p)
|
|
}
|
|
|
|
for idx := range want {
|
|
if p[idx].ResourceID != want[idx] {
|
|
t.Fatalf("at index %d: expected %v, got %v", idx, want[idx], p[idx].ResourceID)
|
|
}
|
|
}
|
|
|
|
if p[0].Status != metav1.ConditionTrue || p[0].Message != "applied" {
|
|
t.Fatalf("expected the tracked item condition to be replaced, got %+v", p[0].ObjectReferenceStatusCondition)
|
|
}
|
|
|
|
if p[1].Message != "keep" || p[2].Message != "keep" {
|
|
t.Fatal("expected the items of the other scopes to keep their condition")
|
|
}
|
|
}
|
|
|
|
func TestProcessedItems_ReplaceScope_EmptyDropsScopeOnly(t *testing.T) {
|
|
now := metav1.NewTime(time.Now())
|
|
|
|
inScope := mkItem("tenant-a", "ns-a", "name-a", "Secret", metav1.ConditionTrue, "Ready", "", true, now)
|
|
otherNs := mkItem("tenant-a", "ns-b", "name-a", "Secret", metav1.ConditionTrue, "Ready", "", true, now)
|
|
|
|
p := meta.ProcessedItems{inScope, otherNs}
|
|
|
|
p.ReplaceScope("tenant-a", "ns-a", nil)
|
|
|
|
if len(p) != 1 || p[0].ResourceID != otherNs.ResourceID {
|
|
t.Fatalf("expected only the out of scope item to survive, got %+v", p)
|
|
}
|
|
}
|