mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-25 16:07:24 +00:00
feat: replicating resources upon namespace creation (#2080)
* 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>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
Oliver Bähler
parent
305cabd4f6
commit
fed61d2815
@@ -9,6 +9,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/runtime/configuration"
|
||||
"github.com/projectcapsule/capsule/pkg/runtime/gvk"
|
||||
)
|
||||
|
||||
type Processor struct {
|
||||
@@ -25,3 +26,15 @@ type ProcessorOptions struct {
|
||||
Force bool
|
||||
Owner *metav1.OwnerReference
|
||||
}
|
||||
|
||||
// Scope narrows a reconciliation down to the items replicated into a
|
||||
// single Namespace of a single Tenant.
|
||||
type Scope struct {
|
||||
Tenant string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
// Matches states whether the given resource identity belongs to the scope.
|
||||
func (s Scope) Matches(id gvk.ResourceID) bool {
|
||||
return id.Tenant == s.Tenant && id.Namespace == s.Namespace
|
||||
}
|
||||
|
||||
@@ -50,6 +50,74 @@ func (p *Processor) Reconcile(
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReconcileNamespace applies the accumulated items targeting a single Namespace of a
|
||||
// single Tenant, returning the processed items of that scope only.
|
||||
//
|
||||
// Contrarily to Reconcile, the given Accumulator is not authoritative for the whole
|
||||
// Tenant: nothing is pruned, nor disowned, since the items missing from it may just
|
||||
// belong to a Namespace this run knows nothing about. Pruning remains a duty of the
|
||||
// full reconciliation.
|
||||
//
|
||||
// The returned items are meant to be grafted on the persisted status through
|
||||
// meta.ProcessedItems.ReplaceScope, which leaves the other Namespaces untouched.
|
||||
// They are returned along an error too, since they carry the outcome of the single
|
||||
// items which have been processed.
|
||||
func (p *Processor) ReconcileNamespace(
|
||||
ctx context.Context,
|
||||
log logr.Logger,
|
||||
c client.Client,
|
||||
current meta.ProcessedItems,
|
||||
acc Accumulator,
|
||||
opts ProcessorOptions,
|
||||
scope Scope,
|
||||
) (meta.ProcessedItems, error) {
|
||||
if scope.Namespace == "" {
|
||||
return nil, fmt.Errorf("cannot process a Namespace scope without a Namespace")
|
||||
}
|
||||
|
||||
log = log.WithValues("tenant", scope.Tenant, "namespace", scope.Namespace)
|
||||
|
||||
processed := current.InScope(scope.Tenant, scope.Namespace)
|
||||
|
||||
scoped, skipped := scopedAccumulator(acc, scope)
|
||||
if skipped > 0 {
|
||||
log.V(5).Info("ignored accumulated items out of the processed scope", "ignored", skipped)
|
||||
}
|
||||
|
||||
log.V(5).Info("starting scoped processing", "present", len(processed), "items", len(scoped))
|
||||
|
||||
if itemErrors := p.applyAccumulatedItems(ctx, log, c, &processed, scoped, opts); itemErrors > 0 {
|
||||
return processed, fmt.Errorf("applying of %d resources failed", itemErrors)
|
||||
}
|
||||
|
||||
log.V(4).Info("scoped processing completed")
|
||||
|
||||
return processed, nil
|
||||
}
|
||||
|
||||
// Retains the accumulated items belonging to the given scope only, along with the
|
||||
// amount of the ignored ones.
|
||||
func scopedAccumulator(acc Accumulator, scope Scope) (Accumulator, int) {
|
||||
scoped := make(Accumulator, len(acc))
|
||||
ignored := 0
|
||||
|
||||
for key, item := range acc {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !scope.Matches(item.Resource) {
|
||||
ignored++
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
scoped[key] = item
|
||||
}
|
||||
|
||||
return scoped, ignored
|
||||
}
|
||||
|
||||
func (p *Processor) pruneProcessedItems(
|
||||
ctx context.Context,
|
||||
log logr.Logger,
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
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"
|
||||
@@ -133,3 +135,146 @@ func TestFailAndRecord(t *testing.T) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user