Document skip-owner-refs on user secrets when deletion disabled

- refresh inline comment in generateSingleUserSecret
- extend enable_owner_references / enable_secrets_deletion docs in
  operator_parameters.md to describe the interaction
- clarify in operator_parameters.md that the protection takes effect
  on the cluster's next sync after the setting is applied
- add third exception in administrator.md "Owner References and Finalizers"
- add TestGenerateSingleUserSecret_OwnerReferences covering all four
  flag combinations plus the cross-namespace cases
This commit is contained in:
Aditya Kumar 2026-08-11 14:54:27 +05:30
parent a01197fc00
commit 50bb2ca733
No known key found for this signature in database
GPG Key ID: 04270D08AE98C919
4 changed files with 151 additions and 5 deletions

View File

@ -285,11 +285,12 @@ will differ and trigger a rolling update of the pods.
## Owner References and Finalizers ## Owner References and Finalizers
The Postgres Operator can set [owner references](https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/) to most of a cluster's child resources to improve The Postgres Operator can set [owner references](https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/) to most of a cluster's child resources to improve
monitoring with GitOps tools and enable cascading deletes. There are two monitoring with GitOps tools and enable cascading deletes. There are three
exceptions: exceptions:
* Persistent Volume Claims, because they are handled by the [PV Reclaim Policy]https://kubernetes.io/docs/tasks/administer-cluster/change-pv-reclaim-policy/ of the Stateful Set * Persistent Volume Claims, because they are handled by the [PV Reclaim Policy]https://kubernetes.io/docs/tasks/administer-cluster/change-pv-reclaim-policy/ of the Stateful Set
* Cross-namespace secrets, because owner references are not allowed across namespaces by design * Cross-namespace secrets, because owner references are not allowed across namespaces by design
* User-credential secrets when [`enable_secrets_deletion`](reference/operator_parameters.md#enable_secrets_deletion) is `false`, so Kubernetes garbage collection does not cascade-delete them after the Postgresql resource is removed (the `enable_secrets_deletion` flag alone only suppresses the operator's own delete path, not K8s GC)
The operator would clean these resources up with its regular delete loop The operator would clean these resources up with its regular delete loop
unless they got synced correctly. If for some reason the initial cluster sync unless they got synced correctly. If for some reason the initial cluster sync

View File

@ -289,8 +289,12 @@ configuration they are grouped under the `kubernetes` key.
* **enable_owner_references** * **enable_owner_references**
The operator can set owner references on its child resources (except PVCs, The operator can set owner references on its child resources (except PVCs,
Patroni config service/endpoint, cross-namespace secrets) to improve cluster Patroni config service/endpoint, cross-namespace secrets) to improve cluster
monitoring and enable cascading deletion. The default is `false`. Warning, monitoring and enable cascading deletion. User-credential secrets are also
enabling this option disables configured delete protection checks (see below). excluded from controller owner references whenever
[enable_secrets_deletion](#enable_secrets_deletion) is `false`, so that
Kubernetes garbage collection does not cascade-delete them when the
Postgresql resource is removed. The default is `false`. Warning, enabling
this option disables configured delete protection checks (see below).
* **delete_annotation_date_key** * **delete_annotation_date_key**
key name for annotation that compares manifest value with current date in the key name for annotation that compares manifest value with current date in the
@ -381,7 +385,15 @@ configuration they are grouped under the `kubernetes` key.
* **enable_secrets_deletion** * **enable_secrets_deletion**
By default, the operator deletes secrets when removing the Postgres cluster By default, the operator deletes secrets when removing the Postgres cluster
manifest. To keep secrets, set this option to `false`. The default is `true`. manifest. To keep secrets, set this option to `false`. Note that this only
guards the operator's own deletion logic; Kubernetes garbage collection can
still remove user-credential secrets when
[enable_owner_references](#enable_owner_references) is `true` because the
Postgresql resource acts as a controller owner. To prevent that, the
operator skips the controller owner reference on user-credential secrets
whenever `enable_secrets_deletion` is `false`, so the two settings work
together. This protection takes effect on the cluster's next sync after
the setting is applied. The default is `true`.
* **enable_persistent_volume_claim_deletion** * **enable_persistent_volume_claim_deletion**
By default, the operator deletes persistent volume claims when removing the By default, the operator deletes persistent volume claims when removing the

View File

@ -1931,7 +1931,12 @@ func (c *Cluster) generateSingleUserSecret(pgUser spec.PgUser) *v1.Secret {
lbls = c.connectionPoolerLabels("", false).MatchLabels lbls = c.connectionPoolerLabels("", false).MatchLabels
} }
// if secret lives in another namespace we cannot set ownerReferences // Skip a controller ownerReference on user-credential secrets when the
// operator is configured to keep them (enable_secrets_deletion=false);
// otherwise Kubernetes garbage collection would still cascade-delete them
// once the owning Postgresql CR is removed, defeating that setting.
// Cross-namespace secrets also have no ownerReference because K8s forbids
// cross-namespace ownerRefs by design.
var ownerReferences []metav1.OwnerReference var ownerReferences []metav1.OwnerReference
secretsDeletionDisabled := c.OpConfig.EnableSecretsDeletion != nil && !*c.OpConfig.EnableSecretsDeletion secretsDeletionDisabled := c.OpConfig.EnableSecretsDeletion != nil && !*c.OpConfig.EnableSecretsDeletion
if secretsDeletionDisabled || (c.Config.OpConfig.EnableCrossNamespaceSecret && c.Postgresql.ObjectMeta.Namespace != pgUser.Namespace) { if secretsDeletionDisabled || (c.Config.OpConfig.EnableCrossNamespaceSecret && c.Postgresql.ObjectMeta.Namespace != pgUser.Namespace) {

View File

@ -2829,6 +2829,134 @@ func TestGeneratePodDisruptionBudget(t *testing.T) {
} }
} }
func TestGenerateSingleUserSecret_OwnerReferences(t *testing.T) {
testName := "Test generateSingleUserSecret owner references"
newCluster := func(ownerRefs, secretsDeletion *bool, crossNamespaceSecret bool) *Cluster {
cfg := Config{
OpConfig: config.Config{
Resources: config.Resources{
ClusterNameLabel: "cluster-name",
PodRoleLabel: "spilo-role",
EnableOwnerReferences: ownerRefs,
},
EnableSecretsDeletion: secretsDeletion,
EnableCrossNamespaceSecret: crossNamespaceSecret,
},
}
pg := acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: "myapp-database",
Namespace: "myapp",
UID: types.UID("myapp-database-uid"),
},
Spec: acidv1.PostgresSpec{TeamID: "myapp", NumberOfInstances: 1},
}
return New(cfg, k8sutil.KubernetesClient{}, pg, logger, eventRecorder)
}
newPgUser := func(namespace string) spec.PgUser {
return spec.PgUser{
Name: "app_user",
Namespace: namespace,
Password: "secret",
}
}
hasControllerOwnerRef := func(cluster *Cluster) func(*v1.Secret) error {
return func(secret *v1.Secret) error {
for _, ref := range secret.OwnerReferences {
if ref.UID == cluster.Postgresql.ObjectMeta.UID &&
ref.Name == cluster.Postgresql.ObjectMeta.Name &&
ref.Controller != nil && *ref.Controller {
return nil
}
}
return fmt.Errorf("expected a controller owner reference pointing at the Postgresql CR, got %#v",
secret.OwnerReferences)
}
}
hasNoControllerOwnerRef := func(cluster *Cluster) func(*v1.Secret) error {
return func(secret *v1.Secret) error {
for _, ref := range secret.OwnerReferences {
if ref.UID == cluster.Postgresql.ObjectMeta.UID && ref.Controller != nil && *ref.Controller {
return fmt.Errorf("expected no controller owner reference, got %#v", secret.OwnerReferences)
}
}
return nil
}
}
tests := []struct {
scenario string
cluster *Cluster
pgUser spec.PgUser
expectControllerOwner bool
}{
{
scenario: "owner refs + secrets deletion enabled (default)",
cluster: newCluster(util.True(), util.True(), false),
pgUser: newPgUser("myapp"),
expectControllerOwner: true,
},
{
scenario: "owner refs enabled, secrets deletion disabled (skip owner ref)",
cluster: newCluster(util.True(), util.False(), false),
pgUser: newPgUser("myapp"),
expectControllerOwner: false,
},
{
scenario: "owner refs enabled, secrets deletion unset (default true)",
cluster: newCluster(util.True(), nil, false),
pgUser: newPgUser("myapp"),
expectControllerOwner: true,
},
{
scenario: "owner refs disabled, secrets deletion enabled",
cluster: newCluster(util.False(), util.True(), false),
pgUser: newPgUser("myapp"),
expectControllerOwner: false,
},
{
scenario: "owner refs disabled, secrets deletion disabled",
cluster: newCluster(util.False(), util.False(), false),
pgUser: newPgUser("myapp"),
expectControllerOwner: false,
},
{
scenario: "cross-namespace secret, owner refs + secrets deletion enabled",
cluster: newCluster(util.True(), util.True(), true),
pgUser: newPgUser("other-ns"),
expectControllerOwner: false,
},
{
scenario: "cross-namespace secret, owner refs enabled, secrets deletion disabled",
cluster: newCluster(util.True(), util.False(), true),
pgUser: newPgUser("other-ns"),
expectControllerOwner: false,
},
}
for _, tt := range tests {
secret := tt.cluster.generateSingleUserSecret(tt.pgUser)
if secret == nil {
t.Errorf("%s [%s]: expected a non-nil secret", testName, tt.scenario)
continue
}
var check func(*v1.Secret) error
if tt.expectControllerOwner {
check = hasControllerOwnerRef(tt.cluster)
} else {
check = hasNoControllerOwnerRef(tt.cluster)
}
if err := check(secret); err != nil {
t.Errorf("%s [%s]: %+v", testName, tt.scenario, err)
}
}
}
func TestGenerateService(t *testing.T) { func TestGenerateService(t *testing.T) {
var spec acidv1.PostgresSpec var spec acidv1.PostgresSpec
var cluster *Cluster var cluster *Cluster