diff --git a/pkg/controllers/application.go b/pkg/controllers/application.go index 8f58fe74..53470e51 100644 --- a/pkg/controllers/application.go +++ b/pkg/controllers/application.go @@ -6,7 +6,9 @@ import ( nais_io_v1 "github.com/nais/liberator/pkg/apis/nais.io/v1" nais_io_v1alpha1 "github.com/nais/liberator/pkg/apis/nais.io/v1alpha1" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" ) type ApplicationReconciler struct { @@ -31,6 +33,13 @@ func (r *ApplicationReconciler) SetupWithManager(mgr ctrl.Manager, opts ...Optio return ctrl.NewControllerManagedBy(mgr). For(&nais_io_v1alpha1.Application{}). Watches(&nais_io_v1.Image{}, handler.EnqueueRequestsFromMapFunc(mapImageToApplicationOrNaisjob)). + WatchesMetadata( + postgresMetadata, + handler.EnqueueRequestsFromMapFunc(mapPostgresToApplications(mgr.GetClient())), + builder.WithPredicates(predicate.AnnotationChangedPredicate{}), + ). WithOptions(asControllerOptions(opts)). Complete(r) } + +// +kubebuilder:rbac:groups=data.nais.io,resources=postgres,verbs=get;list;watch diff --git a/pkg/controllers/naisjob.go b/pkg/controllers/naisjob.go index 0ad53061..612c80f5 100644 --- a/pkg/controllers/naisjob.go +++ b/pkg/controllers/naisjob.go @@ -5,7 +5,9 @@ import ( nais_io_v1 "github.com/nais/liberator/pkg/apis/nais.io/v1" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" ) type NaisjobReconciler struct { @@ -30,6 +32,11 @@ func (r *NaisjobReconciler) SetupWithManager(mgr ctrl.Manager, opts ...Option) e return ctrl.NewControllerManagedBy(mgr). For(&nais_io_v1.Naisjob{}). Watches(&nais_io_v1.Image{}, handler.EnqueueRequestsFromMapFunc(mapImageToApplicationOrNaisjob)). + WatchesMetadata( + postgresMetadata, + handler.EnqueueRequestsFromMapFunc(mapPostgresToNaisjobs(mgr.GetClient())), + builder.WithPredicates(predicate.AnnotationChangedPredicate{}), + ). WithOptions(asControllerOptions(opts)). Complete(r) } diff --git a/pkg/controllers/postgres_watch.go b/pkg/controllers/postgres_watch.go new file mode 100644 index 00000000..c60d7a69 --- /dev/null +++ b/pkg/controllers/postgres_watch.go @@ -0,0 +1,67 @@ +package controllers + +import ( + "context" + + nais_io_v1 "github.com/nais/liberator/pkg/apis/nais.io/v1" + nais_io_v1alpha1 "github.com/nais/liberator/pkg/apis/nais.io/v1alpha1" + log "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// postgresMetadata is a PartialObjectMetadata for watching Postgres CRs. +// Using metadata-only watches avoids pulling full specs. +var postgresMetadata = &metav1.PartialObjectMetadata{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "data.nais.io/v1", + Kind: "Postgres", + }, +} + +// mapPostgresToApplications returns a map function that enqueues Applications +// in the same namespace whose spec.postgres.clusterName matches the Postgres CR name. +func mapPostgresToApplications(kube client.Client) func(ctx context.Context, obj client.Object) []ctrl.Request { + return func(ctx context.Context, obj client.Object) []ctrl.Request { + var apps nais_io_v1alpha1.ApplicationList + err := kube.List(ctx, &apps, client.InNamespace(obj.GetNamespace())) + if err != nil { + log.Errorf("postgres watch: failed to list applications in namespace %s: %v", obj.GetNamespace(), err) + return nil + } + + var requests []ctrl.Request + for _, app := range apps.Items { + if app.Spec.Postgres != nil && app.Spec.Postgres.ClusterName == obj.GetName() { + requests = append(requests, ctrl.Request{ + NamespacedName: client.ObjectKeyFromObject(&app), + }) + } + } + return requests + } +} + +// mapPostgresToNaisjobs returns a map function that enqueues Naisjobs +// in the same namespace whose spec.postgres.clusterName matches the Postgres CR name. +func mapPostgresToNaisjobs(kube client.Client) func(ctx context.Context, obj client.Object) []ctrl.Request { + return func(ctx context.Context, obj client.Object) []ctrl.Request { + var jobs nais_io_v1.NaisjobList + err := kube.List(ctx, &jobs, client.InNamespace(obj.GetNamespace())) + if err != nil { + log.Errorf("postgres watch: failed to list naisjobs in namespace %s: %v", obj.GetNamespace(), err) + return nil + } + + var requests []ctrl.Request + for _, job := range jobs.Items { + if job.Spec.Postgres != nil && job.Spec.Postgres.ClusterName == obj.GetName() { + requests = append(requests, ctrl.Request{ + NamespacedName: client.ObjectKeyFromObject(&job), + }) + } + } + return requests + } +} diff --git a/pkg/controllers/postgres_watch_test.go b/pkg/controllers/postgres_watch_test.go new file mode 100644 index 00000000..293127d0 --- /dev/null +++ b/pkg/controllers/postgres_watch_test.go @@ -0,0 +1,172 @@ +package controllers + +import ( + "context" + "testing" + + nais_io_v1 "github.com/nais/liberator/pkg/apis/nais.io/v1" + nais_io_v1alpha1 "github.com/nais/liberator/pkg/apis/nais.io/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestMapPostgresToApplications(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + _ = nais_io_v1alpha1.AddToScheme(scheme) + _ = nais_io_v1.AddToScheme(scheme) + + matchingApp := &nais_io_v1alpha1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-app", + Namespace: "team-a", + }, + Spec: nais_io_v1alpha1.ApplicationSpec{ + Postgres: &nais_io_v1.Postgres{ + ClusterName: "my-pg", + }, + }, + } + + otherApp := &nais_io_v1alpha1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "other-app", + Namespace: "team-a", + }, + Spec: nais_io_v1alpha1.ApplicationSpec{ + Postgres: &nais_io_v1.Postgres{ + ClusterName: "other-pg", + }, + }, + } + + appWithoutPostgres := &nais_io_v1alpha1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "no-pg-app", + Namespace: "team-a", + }, + Spec: nais_io_v1alpha1.ApplicationSpec{}, + } + + appDifferentNamespace := &nais_io_v1alpha1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "diff-ns-app", + Namespace: "team-b", + }, + Spec: nais_io_v1alpha1.ApplicationSpec{ + Postgres: &nais_io_v1.Postgres{ + ClusterName: "my-pg", + }, + }, + } + + kube := fake.NewClientBuilder(). + WithScheme(scheme). + WithRuntimeObjects(matchingApp, otherApp, appWithoutPostgres, appDifferentNamespace). + Build() + + pgObject := &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-pg", + Namespace: "team-a", + }, + } + + mapFn := mapPostgresToApplications(kube) + requests := mapFn(context.Background(), pgObject) + + if len(requests) != 1 { + t.Fatalf("expected 1 request, got %d: %v", len(requests), requests) + } + if requests[0].Name != "my-app" { + t.Errorf("expected request for 'my-app', got %q", requests[0].Name) + } + if requests[0].Namespace != "team-a" { + t.Errorf("expected namespace 'team-a', got %q", requests[0].Namespace) + } +} + +func TestMapPostgresToNaisjobs(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + _ = nais_io_v1alpha1.AddToScheme(scheme) + _ = nais_io_v1.AddToScheme(scheme) + + matchingJob := &nais_io_v1.Naisjob{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-job", + Namespace: "team-a", + }, + Spec: nais_io_v1.NaisjobSpec{ + Postgres: &nais_io_v1.Postgres{ + ClusterName: "my-pg", + }, + }, + } + + otherJob := &nais_io_v1.Naisjob{ + ObjectMeta: metav1.ObjectMeta{ + Name: "other-job", + Namespace: "team-a", + }, + Spec: nais_io_v1.NaisjobSpec{ + Postgres: &nais_io_v1.Postgres{ + ClusterName: "other-pg", + }, + }, + } + + kube := fake.NewClientBuilder(). + WithScheme(scheme). + WithRuntimeObjects(matchingJob, otherJob). + Build() + + pgObject := &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-pg", + Namespace: "team-a", + }, + } + + mapFn := mapPostgresToNaisjobs(kube) + requests := mapFn(context.Background(), pgObject) + + if len(requests) != 1 { + t.Fatalf("expected 1 request, got %d: %v", len(requests), requests) + } + if requests[0].Name != "my-job" { + t.Errorf("expected request for 'my-job', got %q", requests[0].Name) + } + if requests[0].Namespace != "team-a" { + t.Errorf("expected namespace 'team-a', got %q", requests[0].Namespace) + } +} + +func TestMapPostgresToApplications_NoMatches(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + _ = nais_io_v1alpha1.AddToScheme(scheme) + _ = nais_io_v1.AddToScheme(scheme) + + kube := fake.NewClientBuilder(). + WithScheme(scheme). + Build() + + pgObject := &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "orphan-pg", + Namespace: "team-a", + }, + } + + mapFn := mapPostgresToApplications(kube) + requests := mapFn(context.Background(), pgObject) + + if len(requests) != 0 { + t.Fatalf("expected 0 requests, got %d: %v", len(requests), requests) + } +}