Skip to content

Commit d25b537

Browse files
committed
Fix SSA partial update by using unstructured
1 parent ff47024 commit d25b537

1 file changed

Lines changed: 64 additions & 22 deletions

File tree

internal/controller/postgresdatabase_controller.go

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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
4381
type 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)
321362
func (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

Comments
 (0)