@@ -30,6 +30,7 @@ import (
3030 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3131 "k8s.io/apimachinery/pkg/types"
3232
33+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3334 "k8s.io/apimachinery/pkg/runtime"
3435 ctrl "sigs.k8s.io/controller-runtime"
3536 "sigs.k8s.io/controller-runtime/pkg/client"
@@ -39,6 +40,43 @@ import (
3940 enterprisev4 "github.com/splunk/splunk-operator/api/v4"
4041)
4142
43+ type reconcilePhases string
44+ type conditionTypes string
45+ type conditionReasons string
46+ type clusterReadyStatus string
47+
48+ const (
49+ retryDelay = time .Second * 15
50+ // phases
51+ ready reconcilePhases = "Ready"
52+ pending reconcilePhases = "Pending"
53+ provisioning reconcilePhases = "Provisioning"
54+ failed reconcilePhases = "Failed"
55+
56+ // Conditiontypes
57+ clusterReady conditionTypes = "ClusterReady"
58+ secretsReady conditionTypes = "SecretsReady"
59+ usersReady conditionTypes = "UsersReady"
60+ databasesReady conditionTypes = "DatabasesReady"
61+ privilegesReady conditionTypes = "PrivilegesReady"
62+
63+ // Condition reasons
64+ reasonNotFound conditionReasons = "NotFound"
65+ reasonProvisioning conditionReasons = "Provisioning"
66+ reasonClusterInfoFetchFailed conditionReasons = "ClusterInfoFetchNotPossible"
67+ reasonAvailable conditionReasons = "Available"
68+ reasonConfiguring conditionReasons = "Configuring"
69+ reasonWaitingForCNPG conditionReasons = "WaitingForCNPG"
70+ reasonFailed conditionReasons = "Failed"
71+ reasonCreating conditionReasons = "Creating"
72+
73+ // Cluster status
74+ ClusterNotFound clusterReadyStatus = "NotFound"
75+ ClusterNotReady clusterReadyStatus = "NotReady"
76+ ClusterNoProvisionerRef clusterReadyStatus = "NoProvisionerRef"
77+ ClusterReady clusterReadyStatus = "Ready"
78+ )
79+
4280// PostgresDatabaseReconciler reconciles a PostgresDatabase object
4381type PostgresDatabaseReconciler struct {
4482 client.Client
@@ -96,19 +134,19 @@ func (r *PostgresDatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Req
96134 logger .Info ("Cluster validation done" , "clusterName" , cluster .Name , "status" , clusterStatus )
97135 switch clusterStatus {
98136 case ClusterNotFound :
99- if err := updateStatus (clusterReady , metav1 .ConditionFalse , reasonClusterNotFound , "Cluster CR not found" , pending ); err != nil {
137+ if err := updateStatus (clusterReady , metav1 .ConditionFalse , reasonNotFound , "Cluster CR not found" , pending ); err != nil {
100138 return ctrl.Result {}, err
101139 }
102140 return ctrl.Result {RequeueAfter : 30 * time .Second }, nil
103141
104142 case ClusterNotReady , ClusterNoProvisionerRef :
105- if err := updateStatus (clusterReady , metav1 .ConditionFalse , reasonClusterProvisioning , "Cluster is not in ready state yet" , pending ); err != nil {
143+ if err := updateStatus (clusterReady , metav1 .ConditionFalse , reasonProvisioning , "Cluster is not in ready state yet" , pending ); err != nil {
106144 return ctrl.Result {}, err
107145 }
108146 return ctrl.Result {RequeueAfter : retryDelay }, nil
109147
110148 case ClusterReady :
111- if err := updateStatus (clusterReady , metav1 .ConditionTrue , reasonClusterAvailable , "Cluster is operational" , provisioning ); err != nil {
149+ if err := updateStatus (clusterReady , metav1 .ConditionTrue , reasonAvailable , "Cluster is operational" , provisioning ); err != nil {
112150 return ctrl.Result {}, err
113151 }
114152 }
@@ -153,7 +191,7 @@ func (r *PostgresDatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Req
153191 // Spec is correct, verify status - are users reconciled?
154192 allUsersReady , notReadyUsers , err := r .verifyUsersReady (ctx , postgresDB , cluster )
155193 if err != nil {
156- if statusErr := updateStatus (usersReady , metav1 .ConditionFalse , reasonUsersCreationFailed , fmt .Sprintf ("User creation failed: %v" , err ), failed ); statusErr != nil {
194+ if statusErr := updateStatus (usersReady , metav1 .ConditionFalse , reasonFailed , fmt .Sprintf ("User creation failed: %v" , err ), failed ); statusErr != nil {
157195 logger .Error (statusErr , "Failed to update status" )
158196 }
159197 return ctrl.Result {}, err
@@ -169,7 +207,7 @@ func (r *PostgresDatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Req
169207 }
170208
171209 // All users present in spec and reconciled in status
172- if err := updateStatus (usersReady , metav1 .ConditionTrue , reasonUsersAvailable , fmt .Sprintf ("All %d users in PostgreSQL" , len (desiredUsers )), provisioning ); err != nil {
210+ if err := updateStatus (usersReady , metav1 .ConditionTrue , reasonAvailable , fmt .Sprintf ("All %d users in PostgreSQL" , len (desiredUsers )), provisioning ); err != nil {
173211 return ctrl.Result {}, err
174212 }
175213
@@ -222,7 +260,7 @@ func (r *PostgresDatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Req
222260
223261 // All phases complete - mark as reconciled
224262 postgresDB .Status .ObservedGeneration = postgresDB .Generation
225- if err := updateStatus (databasesReady , metav1 .ConditionTrue , reasonDatabasesAvailable , fmt .Sprintf ("All %d databases ready" , len (postgresDB .Spec .Databases )), ready ); err != nil {
263+ if err := updateStatus (databasesReady , metav1 .ConditionTrue , reasonAvailable , fmt .Sprintf ("All %d databases ready" , len (postgresDB .Spec .Databases )), ready ); err != nil {
226264 return ctrl.Result {}, err
227265 }
228266
@@ -315,8 +353,11 @@ func (r *PostgresDatabaseReconciler) getDatabasesInCNPGSpec(ctx context.Context,
315353 return dbList , nil
316354}
317355
318- // createUsers patches PostgresCluster with managed roles via SSA
319- // PostgresCluster controller will then reconcile these roles to CNPG Cluster
356+ // createUsers patches PostgresCluster.spec.managedRoles via SSA using an unstructured patch.
357+ // Using unstructured avoids the zero-value problem: typed Go structs serialize required fields
358+ // (e.g. spec.class) as "" even when unset, causing SSA to claim ownership and conflict.
359+ // An unstructured map contains ONLY the keys we explicitly set — nothing else leaks.
360+ // PostgresCluster controller will then diff and reconcile these roles to CNPG Cluster.
320361// Returns: error (patch failure only)
321362func (r * PostgresDatabaseReconciler ) createUsers (
322363 ctx context.Context ,
@@ -325,7 +366,7 @@ func (r *PostgresDatabaseReconciler) createUsers(
325366) error {
326367 logger := log .FromContext (ctx )
327368
328- // Build list of roles for this PostgresDatabase
369+ // Build roles as raw maps — only name and ensure, nothing else
329370 allRoles := []enterprisev4.ManagedRole {}
330371 for _ , dbSpec := range postgresDB .Spec .Databases {
331372 dbAdminUser := fmt .Sprintf ("%s_admin" , dbSpec .Name )
@@ -341,19 +382,20 @@ func (r *PostgresDatabaseReconciler) createUsers(
341382 })
342383 }
343384
344- // Patch PostgresCluster with SSA to add our roles
345- // SSA with per-role granularity allows multiple PostgresDatabase CRs to manage different roles
346- rolePatch := & enterprisev4.PostgresCluster {
347- TypeMeta : metav1.TypeMeta {
348- APIVersion : "enterprise.splunk.com/v4" ,
349- Kind : "PostgresCluster" ,
350- },
351- ObjectMeta : metav1.ObjectMeta {
352- Name : cluster .Name ,
353- Namespace : cluster .Namespace ,
354- },
355- Spec : enterprisev4.PostgresClusterSpec {
356- ManagedRoles : allRoles ,
385+ // Construct a minimal unstructured patch — only spec.managedRoles is present.
386+ // No other spec fields (class, storage, instances...) are included, so SSA
387+ // will only claim ownership of the roles we explicitly list.
388+ rolePatch := & unstructured.Unstructured {
389+ Object : map [string ]any {
390+ "apiVersion" : cluster .APIVersion ,
391+ "kind" : cluster .Kind ,
392+ "metadata" : map [string ]any {
393+ "name" : cluster .Name ,
394+ "namespace" : cluster .Namespace ,
395+ },
396+ "spec" : map [string ]any {
397+ "managedRoles" : allRoles ,
398+ },
357399 },
358400 }
359401
0 commit comments