Skip to content

K8SPG-1045: add .spec.tls.certManagementPolicy field#1679

Open
pooknull wants to merge 12 commits into
mainfrom
K8SPG-1045
Open

K8SPG-1045: add .spec.tls.certManagementPolicy field#1679
pooknull wants to merge 12 commits into
mainfrom
K8SPG-1045

Conversation

@pooknull

@pooknull pooknull commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

https://perconadev.atlassian.net/browse/K8SPG-1045

DESCRIPTION

This PR adds .spec.tls.certManagementPolicy which controls how TLS secrets are managed. The following values are supported:

  • auto - the operator creates and maintains TLS secrets. This is the default.
  • userProvidedOnly - the operator never generates or modifies TLS secrets. Reconciliation pauses until every required secret is provided by the user.

When secrets are missing (when userProvidedOnly is used), the cluster reports:

conditions:
- type: TLSSecretsReady
  status: "False"
  reason: TLSSecretsMissing
- type: Progressing
  status: "False"
  reason: Paused

The TLSSecretsReady message lists the missing secret names.

Once these secrets are present, the operator creates the instance StatefulSet so its generated name can be seen by the user.

After that, user should create corresponding <statefulset-name>-certs secret.

User's secrets should contain postgres-operator.crunchydata.com/cluster: <cluster-name> label to trigger reconcile

CHECKLIST

Jira

  • Is the Jira ticket created and referenced properly?
  • Does the Jira ticket have the proper statuses for documentation (Needs Doc) and QA (Needs QA)?
  • Does the Jira ticket link to the proper milestone (Fix Version field)?

Tests

  • Is an E2E test/test case added for the new feature/change?
  • Are unit tests added where appropriate?

Config/Logging/Testability

  • Are all needed new/changed options added to default YAML files?
  • Are all needed new/changed options added to the Helm Chart?
  • Did we add proper logging messages for operator actions?
  • Did we ensure compatibility with the previous version or cluster upgrade process?
  • Does the change support oldest and newest supported PG version?
  • Does the change support oldest and newest supported Kubernetes version?

); err != nil {
return nil, errors.Wrapf(err, "get user-provided instance TLS secret %s", existing.Name)
}
return existing, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're returning a nil *corev1.Secret with a nil error. this is unexpected for caller. can we improve if possible or at least comment in function's godoc

@pooknull pooknull Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_, hasCluster := obj.GetLabels()[naming.LabelCluster]
return hasCluster
}))).
Watches(&corev1.Pod{}, r.watchPods()).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be if I create secret manually? As I understand we don't have labels in secret in this case.

@pooknull pooknull Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are going to expect user to set this cluster label: #1679 (comment)

@hors

hors commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

@pooknull please fix conflicts

Comment on lines +39 to +50
clusters := &v1beta1.PostgresClusterList{}
if err := r.Client.List(ctx, clusters, client.InNamespace(obj.GetNamespace())); err != nil {
return nil
}

requests := make([]reconcile.Request, 0, len(clusters.Items))
for i := range clusters.Items {
requests = append(requests, reconcile.Request{
NamespacedName: client.ObjectKeyFromObject(&clusters.Items[i]),
})
}
return requests

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we sure that we want to do this? i'd force user to add cluster label to the secret instead of blindly triggering reconcile

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you. I have reverted this change: 1c0bbf6

@pooknull
pooknull requested review from egegunes and nmarukovich July 21, 2026 11:01
@JNKPercona

Copy link
Copy Markdown
Collaborator
Test Name Result Time
backup-enable-disable passed 00:00:00
builtin-extensions passed 00:00:00
cert-manager-tls passed 00:00:00
cert-management-policy passed 00:00:00
custom-envs passed 00:00:00
custom-tls passed 00:00:00
database-init-sql passed 00:00:00
demand-backup passed 00:00:00
demand-backup-offline-snapshot passed 00:00:00
dynamic-configuration passed 00:00:00
finalizers passed 00:00:00
init-deploy passed 00:00:00
huge-pages passed 00:00:00
major-upgrade-14-to-15 passed 00:00:00
major-upgrade-15-to-16 passed 00:00:00
major-upgrade-16-to-17 passed 00:00:00
major-upgrade-17-to-18 passed 00:00:00
ldap passed 00:00:00
ldap-tls passed 00:00:00
monitoring passed 00:00:00
monitoring-pmm3 passed 00:00:00
one-pod passed 00:00:00
operator-self-healing passed 00:00:00
pgbouncer-mtls passed 00:00:00
pitr passed 00:00:00
scaling passed 00:00:00
scheduled-backup passed 00:00:00
self-healing passed 00:00:00
sidecars passed 00:00:00
standby-pgbackrest passed 00:00:00
standby-streaming passed 00:00:00
start-from-backup passed 00:00:00
tablespaces passed 00:00:00
telemetry-transfer passed 00:00:00
upgrade-consistency passed 00:00:00
upgrade-minor failure 00:09:43
users passed 00:00:00
migration-from-crunchy-standby passed 00:00:00
migration-from-crunchy-pv passed 00:00:00
migration-from-crunchy-backup-restore passed 00:00:00
Summary Value
Tests Run 40/40
Job Duration 00:44:10
Total Test Time 00:09:43

commit: 312ca39
image: perconalab/percona-postgresql-operator:PR-1679-312ca390f

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new .spec.tls.certManagementPolicy API knob to control whether the operator manages TLS secrets (auto, default) or requires all TLS secrets to be user-provided (userProvidedOnly) and pauses reconciliation until prerequisites are met.

Changes:

  • Introduces certManagementPolicy to the PostgresCluster/PerconaPGCluster CRDs and Go types with defaulting + enum validation.
  • Adds controller logic to detect missing user-provided TLS secrets, set a TLSSecretsReady condition, and pause reconciliation via Progressing=False when secrets are missing.
  • Adds KUTTL E2E coverage for the new policy, and updates generated CRD/bundle artifacts.

Reviewed changes

Copilot reviewed 29 out of 30 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pkg/apis/upstream.pgv2.percona.com/v1beta1/postgrescluster_types.go Adds certManagementPolicy type/enum, defaulting helper, and a new condition type constant.
internal/controller/postgrescluster/watches.go Adds a Secret watch to trigger reconcile when user-managed Secrets change.
internal/controller/postgrescluster/pki.go Implements userProvidedOnly behavior: required-secret detection/conditions and user-provided certificate handling.
internal/controller/postgrescluster/pgbouncer.go Honors userProvidedOnly by requiring an existing PgBouncer TLS secret rather than generating one.
internal/controller/postgrescluster/pgbackrest.go Honors userProvidedOnly by requiring an existing pgBackRest secret when repo-host is present.
internal/controller/postgrescluster/patroni.go Requires an existing replication TLS secret when userProvidedOnly is set.
internal/controller/postgrescluster/instance.go Allows StatefulSet creation while deferring instance cert Secret enforcement to subsequent reconciles.
internal/controller/postgrescluster/controller.go Pauses reconciliation when TLSSecretsReady=False and wires up the new Secret watch.
e2e-tests/tests/cert-management-policy/00-deploy-operator.yaml Test setup for cert-management-policy scenario.
e2e-tests/tests/cert-management-policy/00-assert.yaml Asserts operator deployment readiness for the scenario.
e2e-tests/tests/cert-management-policy/01-create-initial-cluster.yaml Creates an initial cluster in auto mode and enables components for the test.
e2e-tests/tests/cert-management-policy/01-assert.yaml Verifies the baseline cluster is healthy and expected secrets/resources exist.
e2e-tests/tests/cert-management-policy/02-save-secrets.yaml Captures generated TLS secrets for later reuse as “user-provided” secrets.
e2e-tests/tests/cert-management-policy/03-delete-cluster-and-secrets.yaml Deletes the cluster and related resources to prepare for the userProvidedOnly flow.
e2e-tests/tests/cert-management-policy/04-create-user-provided-cluster.yaml Recreates the cluster with certManagementPolicy=userProvidedOnly.
e2e-tests/tests/cert-management-policy/04-assert.yaml Asserts the cluster pauses with TLSSecretsReady=False and Progressing=False.
e2e-tests/tests/cert-management-policy/05-restore-secrets.yaml Restores the saved TLS secrets (except instance certs) to resume partial progress.
e2e-tests/tests/cert-management-policy/05-assert.yaml Asserts the cluster remains paused until instance cert secrets are restored.
e2e-tests/tests/cert-management-policy/06-restore-instance-secret.yaml Restores instance cert secret using the generated StatefulSet name.
e2e-tests/tests/cert-management-policy/06-assert.yaml Verifies full readiness after all user-provided secrets are present.
e2e-tests/tests/cert-management-policy/99-remove-cluster-gracefully.yaml Cleanup step for the E2E scenario (removes resources, checks operator health).
e2e-tests/run-release.csv Adds the new E2E test suite to release test runs.
e2e-tests/run-pr.csv Adds the new E2E test suite to PR test runs.
deploy/cw-bundle.yaml Regenerated bundle content to include the new CRD field.
deploy/crd.yaml Regenerated CRD manifest to include the new CRD field.
deploy/cr.yaml Updates example CR comments to mention certManagementPolicy.
deploy/bundle.yaml Regenerated bundle content to include the new CRD field.
config/crd/bases/upstream.pgv2.percona.com_postgresclusters.yaml Adds certManagementPolicy to the upstream PostgresCluster CRD schema.
config/crd/bases/pgv2.percona.com_perconapgclusters.yaml Adds certManagementPolicy to the PerconaPGCluster CRD schema.
build/crd/percona/generated/pgv2.percona.com_perconapgclusters.yaml Updates generated CRD output to include certManagementPolicy.

Comment on lines +79 to +82
if err := checkSecret(nil, naming.PGBackRestSecret(cluster).Name); err != nil {
return errors.Wrap(err, "check pgBackRest TLS secret")
}

Comment on lines +35 to +47
func (r *Reconciler) reconcileTLSCertManagementPolicy(ctx context.Context, cluster *v1beta1.PostgresCluster) error {
cond := metav1.Condition{
Type: v1beta1.ConditionTypeTLSSecretsReady,
Status: metav1.ConditionTrue,
Reason: "TLSSecretsFound",
ObservedGeneration: cluster.GetGeneration(),
}

if cluster.Spec.TLS.GetCertManagementPolicy() != v1beta1.CertManagementUserProvidedOnly {
cond.Message = "certManagementPolicy is " + string(cluster.Spec.TLS.GetCertManagementPolicy())
meta.SetStatusCondition(&cluster.Status.Conditions, cond)
return nil
}
Comment on lines +22 to +24
// watchClusterSecrets returns a handler.EventHandler for Secrets that are
// labeled with a PostgresCluster name but intentionally have no owner reference.
func (*Reconciler) watchClusterSecrets() handler.EventHandler {
meta.SetStatusCondition(&cluster.Status.Conditions, metav1.Condition{
Type: v1beta1.PostgresClusterProgressing,
Status: metav1.ConditionFalse,
Reason: "Paused",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To not confuse with cluster paused which is different

Suggested change
Reason: "Paused",
Reason: "ReconcilePaused",

Comment on lines +265 to +278
if cond := meta.FindStatusCondition(cluster.Status.Conditions, v1beta1.ConditionTypeTLSSecretsReady); cond != nil {
if cond.Status == metav1.ConditionFalse {
meta.SetStatusCondition(&cluster.Status.Conditions, metav1.Condition{
Type: v1beta1.PostgresClusterProgressing,
Status: metav1.ConditionFalse,
Reason: "Paused",
Message: "Reconciliation is paused. Check `TLSSecretsReady` condition",
ObservedGeneration: cluster.GetGeneration(),
})
return runtime.ErrorWithBackoff(patchClusterStatus())
}
meta.RemoveStatusCondition(&cluster.Status.Conditions, v1beta1.PostgresClusterProgressing)
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ifs can be combined by using IsStatusConditionPresentAndEqual

Suggested change
if cond := meta.FindStatusCondition(cluster.Status.Conditions, v1beta1.ConditionTypeTLSSecretsReady); cond != nil {
if cond.Status == metav1.ConditionFalse {
meta.SetStatusCondition(&cluster.Status.Conditions, metav1.Condition{
Type: v1beta1.PostgresClusterProgressing,
Status: metav1.ConditionFalse,
Reason: "Paused",
Message: "Reconciliation is paused. Check `TLSSecretsReady` condition",
ObservedGeneration: cluster.GetGeneration(),
})
return runtime.ErrorWithBackoff(patchClusterStatus())
}
meta.RemoveStatusCondition(&cluster.Status.Conditions, v1beta1.PostgresClusterProgressing)
}
}
if meta.IsStatusConditionPresentAndEqual(cluster.Status.Conditions, v1beta1.ConditionTypeTLSSecretsReady, metav1.ConditionFalse) {
meta.SetStatusCondition(&cluster.Status.Conditions, metav1.Condition{
Type: v1beta1.PostgresClusterProgressing,
Status: metav1.ConditionFalse,
Reason: "Paused",
Message: "Reconciliation is paused. Check `TLSSecretsReady` condition",
ObservedGeneration: cluster.GetGeneration(),
})
return runtime.ErrorWithBackoff(patchClusterStatus())
}
meta.RemoveStatusCondition(&cluster.Status.Conditions, v1beta1.PostgresClusterProgressing)
}

)

// K8SPG-1045
func (r *Reconciler) reconcileTLSCertManagementPolicy(ctx context.Context, cluster *v1beta1.PostgresCluster) error {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this is only reconciling the status condition

Suggested change
func (r *Reconciler) reconcileTLSCertManagementPolicy(ctx context.Context, cluster *v1beta1.PostgresCluster) error {
func (r *Reconciler) reconcileTLSCondition(ctx context.Context, cluster *v1beta1.PostgresCluster) error {

)

// K8SPG-1045
func (r *Reconciler) reconcileTLSCertManagementPolicy(ctx context.Context, cluster *v1beta1.PostgresCluster) error {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a unit test for this, should be easy with fake client and mock objects

)

// watchClusterSecrets returns a handler.EventHandler for Secrets that are
// labeled with a PostgresCluster name but intentionally have no owner reference.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're not asserting anything about ownerRefs in the function, is it expected?

Comment on lines +189 to +190
_ = root.Certificate.UnmarshalText(existing.Data[certificateKey])
_ = root.PrivateKey.UnmarshalText(existing.Data[privateKey])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

processing the errors could be quite helpful for figuring out what is wrong exactly and make it easier for user to fix it, so maybe we shouldn't skip the errors

@hors hors left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pooknull please check upgrade-minor test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants