mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-26 13:07:04 +09:00
Nothing enforced that spec.application.kind is unique, and each consumer that maps a kind back to a definition resolved a duplicate its own way: the chartRef reconciler let every definition rewrite every release of its kind, the lineage webhook and the CA projection reconciler took whichever came first in an unordered list, and cozystack-api registered one resource per definition. A second definition declaring a kind already in use could therefore repoint that kind's tenant releases at its own artifact. Add one ownership rule, the definition created first owns the kind with ties broken by name, and route every consumer through it. A definition that arrives later with a kind already in use no longer manages that kind's releases whatever it is named: the chartRef reconciler skips it with a Warning event and re-reconciles the next owner when the current one goes, cozystack-api registers the owner only and logs the rest, its restart hash moves when ownership moves, and the lineage and CA projection lookups resolve the owner. Assisted-by: LLM Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
131 lines
4.8 KiB
Go
131 lines
4.8 KiB
Go
package lineagecontrollerwebhook
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
)
|
|
|
|
func newWebhookScheme(t *testing.T) *runtime.Scheme {
|
|
t.Helper()
|
|
scheme := runtime.NewScheme()
|
|
if err := cozyv1alpha1.AddToScheme(scheme); err != nil {
|
|
t.Fatalf("add cozyv1alpha1: %v", err)
|
|
}
|
|
return scheme
|
|
}
|
|
|
|
// TestReconcile_EmptyClusterStoresEmptyMap pins the empty-cluster
|
|
// reconcile: with no ApplicationDefinitions present, the reconciler
|
|
// must store a fresh runtimeConfig with an empty map (not leave the
|
|
// stored config nil, not return an error).
|
|
func TestReconcile_EmptyClusterStoresEmptyMap(t *testing.T) {
|
|
scheme := newWebhookScheme(t)
|
|
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
|
|
|
|
w := &LineageControllerWebhook{Client: fakeClient, Scheme: scheme}
|
|
if _, err := w.Reconcile(context.TODO(), ctrl.Request{}); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
cfg, ok := w.config.Load().(*runtimeConfig)
|
|
if !ok {
|
|
t.Fatalf("expected *runtimeConfig stored, got %T", w.config.Load())
|
|
}
|
|
if cfg.appCRDMap == nil {
|
|
t.Errorf("expected non-nil empty map")
|
|
}
|
|
if len(cfg.appCRDMap) != 0 {
|
|
t.Errorf("expected 0 entries, got %d", len(cfg.appCRDMap))
|
|
}
|
|
}
|
|
|
|
// TestReconcile_BuildsAppCRDMapByKind pins the documented mapping:
|
|
// each ApplicationDefinition is keyed by (group=apps.cozystack.io,
|
|
// kind=Spec.Application.Kind). The map values point to the
|
|
// ApplicationDefinition itself.
|
|
func TestReconcile_BuildsAppCRDMapByKind(t *testing.T) {
|
|
scheme := newWebhookScheme(t)
|
|
|
|
harbor := &cozyv1alpha1.ApplicationDefinition{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "harbor"},
|
|
Spec: cozyv1alpha1.ApplicationDefinitionSpec{
|
|
Application: cozyv1alpha1.ApplicationDefinitionApplication{Kind: "Harbor"},
|
|
},
|
|
}
|
|
bucket := &cozyv1alpha1.ApplicationDefinition{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "bucket"},
|
|
Spec: cozyv1alpha1.ApplicationDefinitionSpec{
|
|
Application: cozyv1alpha1.ApplicationDefinitionApplication{Kind: "Bucket"},
|
|
},
|
|
}
|
|
|
|
fakeClient := fake.NewClientBuilder().
|
|
WithScheme(scheme).
|
|
WithObjects(harbor, bucket).
|
|
Build()
|
|
|
|
w := &LineageControllerWebhook{Client: fakeClient, Scheme: scheme}
|
|
if _, err := w.Reconcile(context.TODO(), ctrl.Request{}); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
cfg := w.config.Load().(*runtimeConfig)
|
|
if len(cfg.appCRDMap) != 2 {
|
|
t.Fatalf("expected 2 entries, got %d (%+v)", len(cfg.appCRDMap), cfg.appCRDMap)
|
|
}
|
|
if got := cfg.appCRDMap[appRef{"apps.cozystack.io", "Harbor"}]; got == nil || got.Name != "harbor" {
|
|
t.Errorf("expected Harbor → harbor entry, got %+v", got)
|
|
}
|
|
if got := cfg.appCRDMap[appRef{"apps.cozystack.io", "Bucket"}]; got == nil || got.Name != "bucket" {
|
|
t.Errorf("expected Bucket → bucket entry, got %+v", got)
|
|
}
|
|
}
|
|
|
|
// TestReconcile_DuplicateKindKeepsOwner pins the duplicate handling: when two
|
|
// ApplicationDefinitions declare the same Application.Kind, the lineage map
|
|
// keeps the owner under the shared rule (created first, then by name), the same
|
|
// definition cozystack-api and the chartRef reconciler treat as the owner. The
|
|
// newcomer is tried on both sides of the owner by name, so neither a first-wins
|
|
// nor a last-wins walk of the list can pass.
|
|
func TestReconcile_DuplicateKindKeepsOwner(t *testing.T) {
|
|
scheme := newWebhookScheme(t)
|
|
base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
for _, newcomerName := range []string{"a-harbor-shadow", "z-harbor-shadow"} {
|
|
t.Run(newcomerName, func(t *testing.T) {
|
|
owner := &cozyv1alpha1.ApplicationDefinition{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "harbor", CreationTimestamp: metav1.NewTime(base)},
|
|
Spec: cozyv1alpha1.ApplicationDefinitionSpec{
|
|
Application: cozyv1alpha1.ApplicationDefinitionApplication{Kind: "Harbor"},
|
|
},
|
|
}
|
|
newcomer := &cozyv1alpha1.ApplicationDefinition{
|
|
ObjectMeta: metav1.ObjectMeta{Name: newcomerName, CreationTimestamp: metav1.NewTime(base.Add(time.Hour))},
|
|
Spec: cozyv1alpha1.ApplicationDefinitionSpec{
|
|
Application: cozyv1alpha1.ApplicationDefinitionApplication{Kind: "Harbor"},
|
|
},
|
|
}
|
|
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(owner, newcomer).Build()
|
|
|
|
w := &LineageControllerWebhook{Client: fakeClient, Scheme: scheme}
|
|
if _, err := w.Reconcile(context.TODO(), ctrl.Request{}); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
cfg := w.config.Load().(*runtimeConfig)
|
|
if len(cfg.appCRDMap) != 1 {
|
|
t.Fatalf("expected exactly one Harbor entry (duplicate dropped), got %d", len(cfg.appCRDMap))
|
|
}
|
|
if got := cfg.appCRDMap[appRef{"apps.cozystack.io", "Harbor"}]; got == nil || got.Name != "harbor" {
|
|
t.Fatalf("expected the owner harbor, got %+v", got)
|
|
}
|
|
})
|
|
}
|
|
}
|