diff --git a/internal/controller/modern_platform_test.go b/internal/controller/modern_platform_test.go index c4bab73..7af658c 100644 --- a/internal/controller/modern_platform_test.go +++ b/internal/controller/modern_platform_test.go @@ -189,6 +189,7 @@ func TestReconcileCNPGClusterRemovesStaleProjectionAfterProjectRecreation(t *tes t.Fatal(err) } desired := cnpgresources.BuildCluster(project, &project.Status.SecretNames) + desired.Default() if !apiequality.Semantic.DeepEqual(updated.Spec.PostgresConfiguration.Parameters, desired.Spec.PostgresConfiguration.Parameters) || !apiequality.Semantic.DeepEqual(updated.Spec.PostgresConfiguration.PgHBA, desired.Spec.PostgresConfiguration.PgHBA) || !apiequality.Semantic.DeepEqual(updated.Spec.PostgresConfiguration.AdditionalLibraries, desired.Spec.PostgresConfiguration.AdditionalLibraries) || @@ -234,6 +235,32 @@ func TestReconcileCNPGClusterRemovesOnlyProjectOwnerAndPreservesLabels(t *testin } } +func TestReconcileCNPGClusterConvergesWithAdmissionDefaultedRetainedCluster(t *testing.T) { + t.Parallel() + + scheme := newPowerSyncTestScheme(t) + project := &supabasev1alpha1.SupabaseProject{ + ObjectMeta: metav1.ObjectMeta{Name: "retained", Namespace: "default"}, + Status: supabasev1alpha1.SupabaseProjectStatus{SecretNames: supabasev1alpha1.SecretNamesStatus{ + SupabaseAdmin: "retained-admin", Authenticator: "retained-authenticator", AuthAdmin: "retained-auth-admin", + }}, + } + existing := cnpgresources.BuildCluster(project, &project.Status.SecretNames) + existing.Default() // Simulate CNPG admission defaults on a retained Cluster. + reconciler := &SupabaseProjectReconciler{ + Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(project, existing).Build(), + Scheme: scheme, + } + + result, err := reconciler.reconcileCNPGCluster(context.Background(), project) + if err != nil { + t.Fatalf("reconcileCNPGCluster() error = %v", err) + } + if !result.IsZero() { + t.Fatalf("reconcileCNPGCluster() result = %#v, want no requeue for converged retained Cluster", result) + } +} + func TestDurableAdoptionRequiresMatchingInstanceLabel(t *testing.T) { t.Parallel() diff --git a/internal/controller/supabaseproject_controller.go b/internal/controller/supabaseproject_controller.go index 028b4bf..1256219 100644 --- a/internal/controller/supabaseproject_controller.go +++ b/internal/controller/supabaseproject_controller.go @@ -505,6 +505,10 @@ func (r *SupabaseProjectReconciler) reconcileCNPGCluster(ctx context.Context, pr log.Info("Reconciling CNPG Cluster") desired := cnpg.BuildCluster(project, &project.Status.SecretNames) + // CNPG admission defaults persisted Clusters before they reach this + // reconciler. Apply the same defaults to desired state so retained Clusters + // converge instead of being rewritten on every reconciliation. + desired.Default() // Check if cluster exists existing := &cnpgv1.Cluster{} diff --git a/internal/resources/cnpg/cluster.go b/internal/resources/cnpg/cluster.go index 1eb58af..eb70768 100644 --- a/internal/resources/cnpg/cluster.go +++ b/internal/resources/cnpg/cluster.go @@ -119,6 +119,7 @@ func BuildCluster(project *supabasev1alpha1.SupabaseProject, secretNames *supaba cluster.Spec.Plugins = []cnpgv1.PluginConfiguration{ { Name: BarmanCloudPluginName, + Enabled: ptr.To(true), IsWALArchiver: ptr.To(true), Parameters: map[string]string{ "barmanObjectName": ObjectStoreName(project), @@ -214,7 +215,7 @@ func buildAllRoles(project *supabasev1alpha1.SupabaseProject, secretNames *supab // - powersync_storage: stores Powersync's internal sync state (checkpoints, buckets) // - powersync_replication: reads the WAL via logical replication for CDC func BuildPowersyncRoles(secretNames *supabasev1alpha1.SecretNamesStatus) []cnpgv1.RoleConfiguration { - return []cnpgv1.RoleConfiguration{ + return applyGeneratedRoleDefaults([]cnpgv1.RoleConfiguration{ { Name: "powersync_storage", Ensure: cnpgv1.EnsurePresent, @@ -235,7 +236,7 @@ func BuildPowersyncRoles(secretNames *supabasev1alpha1.SecretNamesStatus) []cnpg }, Comment: "Powersync CDC replication role", }, - } + }) } // buildRoles creates the managed roles for Supabase @@ -309,12 +310,27 @@ func buildRoles(spec *supabasev1alpha1.DatabaseSpec, secretNames *supabasev1alph }, } + // Apply CNPG CRD schema defaults only to roles generated by this operator. + // User-supplied additional roles have already been defaulted by the + // SupabaseProject admission schema and may intentionally set zero values. + roles = applyGeneratedRoleDefaults(roles) + // Add additional custom roles (directly from CNPG RoleConfiguration) roles = append(roles, spec.AdditionalRoles...) return roles } +func applyGeneratedRoleDefaults(roles []cnpgv1.RoleConfiguration) []cnpgv1.RoleConfiguration { + for i := range roles { + roles[i].ConnectionLimit = -1 + if roles[i].Inherit == nil { + roles[i].Inherit = ptr.To(true) + } + } + return roles +} + // defaultParameters returns the default PostgreSQL parameters func defaultParameters() map[string]string { return map[string]string{