diff --git a/build/crd/percona/generated/pgv2.percona.com_perconapgclusters.yaml b/build/crd/percona/generated/pgv2.percona.com_perconapgclusters.yaml index b8502bcc2..ffe91d80f 100644 --- a/build/crd/percona/generated/pgv2.percona.com_perconapgclusters.yaml +++ b/build/crd/percona/generated/pgv2.percona.com_perconapgclusters.yaml @@ -28817,6 +28817,12 @@ spec: properties: caValidityDuration: type: string + certManagementPolicy: + default: auto + enum: + - auto + - userProvidedOnly + type: string certValidityDuration: type: string issuerConf: diff --git a/config/crd/bases/pgv2.percona.com_perconapgclusters.yaml b/config/crd/bases/pgv2.percona.com_perconapgclusters.yaml index 4c0846933..b1ab91802 100644 --- a/config/crd/bases/pgv2.percona.com_perconapgclusters.yaml +++ b/config/crd/bases/pgv2.percona.com_perconapgclusters.yaml @@ -29515,6 +29515,12 @@ spec: properties: caValidityDuration: type: string + certManagementPolicy: + default: auto + enum: + - auto + - userProvidedOnly + type: string certValidityDuration: type: string issuerConf: diff --git a/config/crd/bases/upstream.pgv2.percona.com_postgresclusters.yaml b/config/crd/bases/upstream.pgv2.percona.com_postgresclusters.yaml index bcce3a5c0..c5c660714 100644 --- a/config/crd/bases/upstream.pgv2.percona.com_postgresclusters.yaml +++ b/config/crd/bases/upstream.pgv2.percona.com_postgresclusters.yaml @@ -28759,6 +28759,12 @@ spec: properties: caValidityDuration: type: string + certManagementPolicy: + default: auto + enum: + - auto + - userProvidedOnly + type: string certValidityDuration: type: string issuerConf: diff --git a/deploy/bundle.yaml b/deploy/bundle.yaml index 3ec548090..b17eff940 100644 --- a/deploy/bundle.yaml +++ b/deploy/bundle.yaml @@ -29812,6 +29812,12 @@ spec: properties: caValidityDuration: type: string + certManagementPolicy: + default: auto + enum: + - auto + - userProvidedOnly + type: string certValidityDuration: type: string issuerConf: @@ -66984,6 +66990,12 @@ spec: properties: caValidityDuration: type: string + certManagementPolicy: + default: auto + enum: + - auto + - userProvidedOnly + type: string certValidityDuration: type: string issuerConf: diff --git a/deploy/cr.yaml b/deploy/cr.yaml index d015200e7..239b118ba 100644 --- a/deploy/cr.yaml +++ b/deploy/cr.yaml @@ -59,6 +59,7 @@ spec: # name: replication1-cert # tlsOnly: false # tls: +# certManagementPolicy: auto # certValidityDuration: 2160h # caValidityDuration: 26280h # pgBackRestCertValidityDuration: 2160h diff --git a/deploy/crd.yaml b/deploy/crd.yaml index dc13656b0..21d692016 100644 --- a/deploy/crd.yaml +++ b/deploy/crd.yaml @@ -29812,6 +29812,12 @@ spec: properties: caValidityDuration: type: string + certManagementPolicy: + default: auto + enum: + - auto + - userProvidedOnly + type: string certValidityDuration: type: string issuerConf: @@ -66984,6 +66990,12 @@ spec: properties: caValidityDuration: type: string + certManagementPolicy: + default: auto + enum: + - auto + - userProvidedOnly + type: string certValidityDuration: type: string issuerConf: diff --git a/deploy/cw-bundle.yaml b/deploy/cw-bundle.yaml index d32403cc3..30f250f9d 100644 --- a/deploy/cw-bundle.yaml +++ b/deploy/cw-bundle.yaml @@ -29812,6 +29812,12 @@ spec: properties: caValidityDuration: type: string + certManagementPolicy: + default: auto + enum: + - auto + - userProvidedOnly + type: string certValidityDuration: type: string issuerConf: @@ -66984,6 +66990,12 @@ spec: properties: caValidityDuration: type: string + certManagementPolicy: + default: auto + enum: + - auto + - userProvidedOnly + type: string certValidityDuration: type: string issuerConf: diff --git a/e2e-tests/run-pr.csv b/e2e-tests/run-pr.csv index 24ac4377e..5f48082a2 100644 --- a/e2e-tests/run-pr.csv +++ b/e2e-tests/run-pr.csv @@ -2,6 +2,7 @@ backup-enable-disable builtin-extensions custom-extensions cert-manager-tls +cert-management-policy custom-envs custom-tls database-init-sql diff --git a/e2e-tests/run-release.csv b/e2e-tests/run-release.csv index d42d9d431..c282cfd28 100644 --- a/e2e-tests/run-release.csv +++ b/e2e-tests/run-release.csv @@ -1,6 +1,7 @@ backup-enable-disable builtin-extensions cert-manager-tls +cert-management-policy custom-envs custom-extensions custom-tls diff --git a/e2e-tests/tests/cert-management-policy/00-assert.yaml b/e2e-tests/tests/cert-management-policy/00-assert.yaml new file mode 100644 index 000000000..ae5a062d8 --- /dev/null +++ b/e2e-tests/tests/cert-management-policy/00-assert.yaml @@ -0,0 +1,24 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 120 +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: perconapgclusters.pgv2.percona.com +spec: + group: pgv2.percona.com + names: + kind: PerconaPGCluster + listKind: PerconaPGClusterList + plural: perconapgclusters + singular: perconapgcluster + scope: Namespaced +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: check-operator-deploy-status +timeout: 120 +commands: + - script: kubectl assert exist-enhanced deployment percona-postgresql-operator -n ${OPERATOR_NS:-$NAMESPACE} --field-selector status.readyReplicas=1 diff --git a/e2e-tests/tests/cert-management-policy/00-deploy-operator.yaml b/e2e-tests/tests/cert-management-policy/00-deploy-operator.yaml new file mode 100644 index 000000000..4dc6d5cbe --- /dev/null +++ b/e2e-tests/tests/cert-management-policy/00-deploy-operator.yaml @@ -0,0 +1,13 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +timeout: 120 +commands: + - script: |- + set -o errexit + set -o xtrace + + source ../../functions + init_temp_dir + + destroy_cert_manager + deploy_operator diff --git a/e2e-tests/tests/cert-management-policy/01-assert.yaml b/e2e-tests/tests/cert-management-policy/01-assert.yaml new file mode 100644 index 000000000..1f6cfdb0c --- /dev/null +++ b/e2e-tests/tests/cert-management-policy/01-assert.yaml @@ -0,0 +1,79 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 300 +--- +apiVersion: v1 +kind: Secret +metadata: + name: cert-management-policy-cluster-ca-cert +--- +apiVersion: v1 +kind: Secret +metadata: + name: cert-management-policy-cluster-cert +--- +apiVersion: v1 +kind: Secret +metadata: + name: cert-management-policy-replication-cert +--- +kind: StatefulSet +apiVersion: apps/v1 +metadata: + labels: + postgres-operator.crunchydata.com/cluster: cert-management-policy + postgres-operator.crunchydata.com/data: postgres + postgres-operator.crunchydata.com/instance-set: instance1 +status: + observedGeneration: 1 + replicas: 1 + readyReplicas: 1 + currentReplicas: 1 + updatedReplicas: 1 + collisionCount: 0 +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: cert-management-policy-pgbouncer + labels: + postgres-operator.crunchydata.com/cluster: cert-management-policy + postgres-operator.crunchydata.com/role: pgbouncer +status: + observedGeneration: 1 + replicas: 1 + updatedReplicas: 1 + readyReplicas: 1 +--- +apiVersion: upstream.pgv2.percona.com/v1beta1 +kind: PostgresCluster +metadata: + name: cert-management-policy +status: + instances: + - name: instance1 + readyReplicas: 1 + replicas: 1 + updatedReplicas: 1 + observedGeneration: 1 + proxy: + pgBouncer: + readyReplicas: 1 + replicas: 1 +--- +apiVersion: pgv2.percona.com/v2 +kind: PerconaPGCluster +metadata: + name: cert-management-policy +status: + pgbouncer: + ready: 1 + size: 1 + postgres: + instances: + - name: instance1 + ready: 1 + size: 1 + ready: 1 + size: 1 + state: ready diff --git a/e2e-tests/tests/cert-management-policy/01-create-initial-cluster.yaml b/e2e-tests/tests/cert-management-policy/01-create-initial-cluster.yaml new file mode 100644 index 000000000..dc15a63f4 --- /dev/null +++ b/e2e-tests/tests/cert-management-policy/01-create-initial-cluster.yaml @@ -0,0 +1,15 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +timeout: 10 +commands: + - script: |- + set -o errexit + set -o xtrace + + source ../../functions + + get_cr "cert-management-policy" \ + | yq '.spec.tls.certManagementPolicy = "auto"' \ + | yq '.spec.proxy.pgBouncer.replicas = 1' \ + | yq '.spec.instances[].replicas = 1' \ + | kubectl -n "$NAMESPACE" apply -f - diff --git a/e2e-tests/tests/cert-management-policy/02-save-secrets.yaml b/e2e-tests/tests/cert-management-policy/02-save-secrets.yaml new file mode 100644 index 000000000..d66854c5c --- /dev/null +++ b/e2e-tests/tests/cert-management-policy/02-save-secrets.yaml @@ -0,0 +1,33 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +timeout: 180 +commands: + - script: |- + set -o errexit + set -o pipefail + set -o xtrace + + source ../../functions + + cluster="cert-management-policy" + instance=$(kubectl -n "$NAMESPACE" get statefulset \ + -l postgres-operator.crunchydata.com/cluster="$cluster",postgres-operator.crunchydata.com/data=postgres \ + -o jsonpath='{.items[0].metadata.name}') + + kubectl -n "$NAMESPACE" get secret \ + "${cluster}-cluster-ca-cert" \ + "${cluster}-cluster-cert" \ + "${cluster}-replication-cert" \ + "${cluster}-pgbackrest" \ + "${cluster}-pgbouncer" \ + "${instance}-certs" \ + -o yaml \ + | yq eval 'del( + .metadata, + .items[].metadata.creationTimestamp, + .items[].metadata.managedFields, + .items[].metadata.ownerReferences, + .items[].metadata.resourceVersion, + .items[].metadata.uid + )' - \ + >"$TEMP_DIR/tls-secrets.yaml" diff --git a/e2e-tests/tests/cert-management-policy/03-delete-cluster-and-secrets.yaml b/e2e-tests/tests/cert-management-policy/03-delete-cluster-and-secrets.yaml new file mode 100644 index 000000000..c89a5a6ec --- /dev/null +++ b/e2e-tests/tests/cert-management-policy/03-delete-cluster-and-secrets.yaml @@ -0,0 +1,30 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +timeout: 180 +commands: + - script: |- + set -o errexit + set -o pipefail + set -o xtrace + + source ../../functions + + cluster="cert-management-policy" + + kubectl -n "$NAMESPACE" delete perconapgcluster "$cluster" --wait=false + remove_all_finalizers + kubectl -n "$NAMESPACE" wait --for=delete "perconapgcluster/$cluster" --timeout=120s + kubectl -n "$NAMESPACE" wait --for=delete "postgrescluster/$cluster" --timeout=120s + + kubectl -n "$NAMESPACE" delete pvc \ + -l postgres-operator.crunchydata.com/cluster="$cluster" \ + --ignore-not-found + + kubectl -n "$NAMESPACE" delete secret \ + -l postgres-operator.crunchydata.com/cluster="$cluster" \ + --ignore-not-found + kubectl -n "$NAMESPACE" delete secret \ + "${cluster}-cluster-ca-cert" \ + "${cluster}-cluster-cert" \ + "${cluster}-replication-cert" \ + --ignore-not-found diff --git a/e2e-tests/tests/cert-management-policy/04-assert.yaml b/e2e-tests/tests/cert-management-policy/04-assert.yaml new file mode 100644 index 000000000..d8632c239 --- /dev/null +++ b/e2e-tests/tests/cert-management-policy/04-assert.yaml @@ -0,0 +1,19 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 60 +--- +apiVersion: pgv2.percona.com/v2 +kind: PerconaPGCluster +metadata: + name: cert-management-policy +status: + conditions: + - reason: PGBackRestRepoHostReady + status: "False" + type: ReadyForBackup + - reason: TLSSecretsMissing + status: "False" + type: TLSSecretsReady + - reason: Paused + status: "False" + type: Progressing diff --git a/e2e-tests/tests/cert-management-policy/04-create-user-provided-cluster.yaml b/e2e-tests/tests/cert-management-policy/04-create-user-provided-cluster.yaml new file mode 100644 index 000000000..c9620a0ba --- /dev/null +++ b/e2e-tests/tests/cert-management-policy/04-create-user-provided-cluster.yaml @@ -0,0 +1,17 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: |- + set -o errexit + set -o pipefail + set -o xtrace + + source ../../functions + + cluster="cert-management-policy" + + get_cr "$cluster" \ + | yq '.spec.tls.certManagementPolicy = "userProvidedOnly"' \ + | yq '.spec.proxy.pgBouncer.replicas = 1' \ + | yq '.spec.instances[].replicas = 1' \ + | kubectl -n "$NAMESPACE" apply -f - diff --git a/e2e-tests/tests/cert-management-policy/05-assert.yaml b/e2e-tests/tests/cert-management-policy/05-assert.yaml new file mode 100644 index 000000000..58d79931e --- /dev/null +++ b/e2e-tests/tests/cert-management-policy/05-assert.yaml @@ -0,0 +1,36 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 180 +--- +kind: StatefulSet +apiVersion: apps/v1 +metadata: + labels: + postgres-operator.crunchydata.com/cluster: cert-management-policy + postgres-operator.crunchydata.com/data: postgres + postgres-operator.crunchydata.com/instance-set: instance1 +--- +apiVersion: pgv2.percona.com/v2 +kind: PerconaPGCluster +metadata: + name: cert-management-policy +status: + conditions: + - reason: PGBackRestRepoHostReady + status: "False" + type: ReadyForBackup + - reason: TLSSecretsMissing + status: "False" + type: TLSSecretsReady + - reason: Paused + status: "False" + type: Progressing + - reason: RepoHostNotReady + status: "False" + type: PGBackRestRepoHostReady + - reason: StanzaNotCreated + status: "False" + type: PGBackRestReplicaRepoReady + - reason: RepoBackupNotComplete + status: "False" + type: PGBackRestReplicaCreate diff --git a/e2e-tests/tests/cert-management-policy/05-restore-secrets.yaml b/e2e-tests/tests/cert-management-policy/05-restore-secrets.yaml new file mode 100644 index 000000000..8f0a7a5bb --- /dev/null +++ b/e2e-tests/tests/cert-management-policy/05-restore-secrets.yaml @@ -0,0 +1,13 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: |- + set -o errexit + set -o xtrace + + source ../../functions + + yq eval \ + 'del(.items[] | select(.metadata.name | test("-certs$")))' \ + "$TEMP_DIR/tls-secrets.yaml" \ + | kubectl -n "$NAMESPACE" apply -f - diff --git a/e2e-tests/tests/cert-management-policy/06-assert.yaml b/e2e-tests/tests/cert-management-policy/06-assert.yaml new file mode 100644 index 000000000..ff107dfb3 --- /dev/null +++ b/e2e-tests/tests/cert-management-policy/06-assert.yaml @@ -0,0 +1,60 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 600 +--- +apiVersion: v1 +kind: Secret +metadata: + name: cert-management-policy-cluster-ca-cert +--- +apiVersion: v1 +kind: Secret +metadata: + name: cert-management-policy-cluster-cert +--- +apiVersion: v1 +kind: Secret +metadata: + name: cert-management-policy-replication-cert +--- +apiVersion: v1 +kind: Secret +metadata: + name: cert-management-policy-pgbackrest +--- +apiVersion: v1 +kind: Secret +metadata: + name: cert-management-policy-pgbouncer +--- +kind: StatefulSet +apiVersion: apps/v1 +metadata: + labels: + postgres-operator.crunchydata.com/cluster: cert-management-policy + postgres-operator.crunchydata.com/data: postgres + postgres-operator.crunchydata.com/instance-set: instance1 +status: + replicas: 1 + readyReplicas: 1 +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: cert-management-policy-pgbouncer +status: + replicas: 1 + readyReplicas: 1 +--- +apiVersion: pgv2.percona.com/v2 +kind: PerconaPGCluster +metadata: + name: cert-management-policy +status: + pgbouncer: + ready: 1 + size: 1 + postgres: + ready: 1 + size: 1 + state: ready diff --git a/e2e-tests/tests/cert-management-policy/06-restore-instance-secret.yaml b/e2e-tests/tests/cert-management-policy/06-restore-instance-secret.yaml new file mode 100644 index 000000000..5b5aaeb80 --- /dev/null +++ b/e2e-tests/tests/cert-management-policy/06-restore-instance-secret.yaml @@ -0,0 +1,23 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: |- + set -o errexit + set -o pipefail + set -o xtrace + + source ../../functions + + cluster="cert-management-policy" + instance=$(kubectl -n "$NAMESPACE" get statefulset \ + -l postgres-operator.crunchydata.com/cluster="$cluster",postgres-operator.crunchydata.com/data=postgres \ + -o jsonpath='{.items[0].metadata.name}') + + new_instance_secret="${instance}-certs" + + instance="$instance" new_instance_secret="$new_instance_secret" yq eval \ + '.items[] | select(.metadata.name | test("-certs$")) | + .metadata.name = strenv(new_instance_secret) | + .metadata.labels."postgres-operator.crunchydata.com/instance" = strenv(instance)' \ + "$TEMP_DIR/tls-secrets.yaml" \ + | kubectl -n "$NAMESPACE" apply -f - diff --git a/e2e-tests/tests/cert-management-policy/99-remove-cluster-gracefully.yaml b/e2e-tests/tests/cert-management-policy/99-remove-cluster-gracefully.yaml new file mode 100644 index 000000000..f7840e50f --- /dev/null +++ b/e2e-tests/tests/cert-management-policy/99-remove-cluster-gracefully.yaml @@ -0,0 +1,31 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: +- apiVersion: pgv2.percona.com/v2 + kind: PerconaPGCluster + metadata: + name: cert-management-policy +- apiVersion: upstream.pgv2.percona.com/v1beta1 + kind: PostgresCluster + metadata: + name: cert-management-policy +commands: + - script: |- + set -o errexit + set -o xtrace + + source ../../functions + + cluster="cert-management-policy" + + remove_all_finalizers + kubectl -n "$NAMESPACE" delete pvc -l postgres-operator.crunchydata.com/cluster="$cluster" || true + kubectl -n "$NAMESPACE" delete secret -l postgres-operator.crunchydata.com/cluster="$cluster" || true + kubectl -n "$NAMESPACE" delete secret \ + "${cluster}-cluster-ca-cert" \ + "${cluster}-cluster-cert" \ + "${cluster}-replication-cert" \ + --ignore-not-found || true + check_operator_panic + destroy_operator + timeout: 60 diff --git a/e2e-tests/tests/upgrade-minor/05-assert.yaml b/e2e-tests/tests/upgrade-minor/05-assert.yaml index 961bcbabc..a3bc8603e 100644 --- a/e2e-tests/tests/upgrade-minor/05-assert.yaml +++ b/e2e-tests/tests/upgrade-minor/05-assert.yaml @@ -139,6 +139,9 @@ status: - type: APIGroupMigration reason: APIGroupMigrationNotNeeded status: "True" + - type: TLSSecretsReady + reason: TLSSecretsFound + status: "True" - type: PGTDEEnabled reason: Disabled status: "False" diff --git a/internal/controller/postgrescluster/controller.go b/internal/controller/postgrescluster/controller.go index 16c9f409e..8df489f24 100644 --- a/internal/controller/postgrescluster/controller.go +++ b/internal/controller/postgrescluster/controller.go @@ -257,6 +257,25 @@ func (r *Reconciler) Reconcile( } } + // K8SPG-1045 + if err == nil { + if err = r.reconcileTLSCondition(ctx, cluster); err != nil { + return runtime.ErrorWithBackoff(err) + } + + 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) + } + pgHBAs := postgres.NewHBAs() pmm.PostgreSQLHBAs(cluster, &pgHBAs) pgmonitor.PostgreSQLHBAs(cluster, &pgHBAs) @@ -621,6 +640,10 @@ func (r *Reconciler) SetupWithManager(mgr manager.Manager) error { Owns(&rbacv1.RoleBinding{}). Owns(&batchv1.CronJob{}). Owns(&policyv1.PodDisruptionBudget{}). + Watches(&corev1.Secret{}, r.watchClusterSecrets(), builder.WithPredicates(predicate.NewPredicateFuncs(func(obj client.Object) bool { + _, hasCluster := obj.GetLabels()[naming.LabelCluster] + return hasCluster + }))). Watches(&corev1.Pod{}, r.watchPods()). Watches(&corev1.Secret{}, r.watchPGBouncerUserSecrets()). Watches(&appsv1.StatefulSet{}, diff --git a/internal/controller/postgrescluster/instance.go b/internal/controller/postgrescluster/instance.go index 8446e3012..ee87e04d1 100644 --- a/internal/controller/postgrescluster/instance.go +++ b/internal/controller/postgrescluster/instance.go @@ -1551,13 +1551,28 @@ func (r *Reconciler) reconcileInstanceConfigMap( // managed, it creates a Certificate CR and augments the resulting secret with // Patroni and pgBackRest keys. Otherwise, it uses internal PKI — first // reconciling any stale Certificate CR left by K8SPG-1017 to update its -// ownerRef (K8SPG-1007 recovery). +// ownerRef (K8SPG-1007 recovery). When userProvidedOnly is specified, it +// returns the existing user-provided Secret, or a placeholder containing its +// name and namespace if it does not exist. func (r *Reconciler) reconcileInstanceCertificates( ctx context.Context, cluster *v1beta1.PostgresCluster, spec *v1beta1.PostgresInstanceSetSpec, instance *appsv1.StatefulSet, rootCertificateAuth *pki.RootCertificateAuthority, ) (*corev1.Secret, error) { if cluster.Spec.CustomTLSSecret == nil { + if cluster.Spec.TLS.GetCertManagementPolicy() == v1beta1.CertManagementUserProvidedOnly { + existing := &corev1.Secret{ObjectMeta: naming.InstanceCertificates(instance)} + // Allow the StatefulSet to be created so its generated name is visible + // to the user. The next reconciliation checks its certificate Secret + // and pauses until the user provides it. + if err := client.IgnoreNotFound( + r.Client.Get(ctx, client.ObjectKeyFromObject(existing), existing), + ); err != nil { + return nil, errors.Wrapf(err, "get user-provided instance TLS secret %s", existing.Name) + } + return existing, nil + } + certManagerManaged, err := r.isRootCACertManagerManaged(ctx, cluster) if err != nil { return nil, errors.Wrap(err, "failed to check if cert-manager manages root CA") diff --git a/internal/controller/postgrescluster/patroni.go b/internal/controller/postgrescluster/patroni.go index 858b6cafd..ec61fece4 100644 --- a/internal/controller/postgrescluster/patroni.go +++ b/internal/controller/postgrescluster/patroni.go @@ -37,10 +37,12 @@ func (r *Reconciler) deletePatroniArtifacts( selector, err := naming.AsSelector(naming.ClusterPatronis(cluster)) if err == nil { err = errors.WithStack( - r.Client.DeleteAllOf(ctx, &corev1.Endpoints{}, + r.Client.DeleteAllOf( + ctx, &corev1.Endpoints{}, client.InNamespace(cluster.Namespace), client.MatchingLabelsSelector{Selector: selector}, - )) + ), + ) } return err @@ -158,13 +160,15 @@ func (r *Reconciler) reconcilePatroniDistributedConfiguration( err := errors.WithStack(r.setControllerReference(cluster, dcsService)) dcsService.Annotations = naming.Merge( - cluster.Spec.Metadata.GetAnnotationsOrNil()) + cluster.Spec.Metadata.GetAnnotationsOrNil(), + ) dcsService.Labels = naming.Merge( cluster.Spec.Metadata.GetLabelsOrNil(), naming.WithPerconaLabels(map[string]string{ // K8SPG-430 naming.LabelCluster: cluster.Name, naming.LabelPatroni: naming.PatroniScope(cluster), - }, cluster.Name, "", cluster.Labels[naming.LabelVersion])) + }, cluster.Name, "", cluster.Labels[naming.LabelVersion]), + ) // Allocate no IP address (headless) and create no Endpoints. // - https://docs.k8s.io/concepts/services-networking/service/#headless-services @@ -225,7 +229,8 @@ func (r *Reconciler) reconcilePatroniDynamicConfiguration( logging.FromContext(ctx).V(1).Info("Replacing patroni dynamic configuration") return errors.WithStack( - patroni.Executor(exec).ReplaceConfiguration(ctx, configuration)) + patroni.Executor(exec).ReplaceConfiguration(ctx, configuration), + ) } // generatePatroniLeaderLeaseService returns a v1.Service that exposes the @@ -237,9 +242,11 @@ func (r *Reconciler) generatePatroniLeaderLeaseService( service.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Service")) service.Annotations = naming.Merge( - cluster.Spec.Metadata.GetAnnotationsOrNil()) + cluster.Spec.Metadata.GetAnnotationsOrNil(), + ) service.Labels = naming.Merge( - cluster.Spec.Metadata.GetLabelsOrNil()) + cluster.Spec.Metadata.GetLabelsOrNil(), + ) if spec := cluster.Spec.Service; spec != nil { service.Annotations = naming.Merge(service.Annotations, @@ -338,7 +345,8 @@ func (r *Reconciler) reconcilePatroniStatus( dcs := &corev1.Endpoints{ObjectMeta: naming.PatroniDistributedConfiguration(cluster)} err := errors.WithStack(client.IgnoreNotFound( - r.Client.Get(ctx, client.ObjectKeyFromObject(dcs), dcs))) + r.Client.Get(ctx, client.ObjectKeyFromObject(dcs), dcs), + )) if err == nil { if dcs.Annotations["initialize"] != "" { @@ -380,6 +388,14 @@ func (r *Reconciler) reconcileReplicationSecret( return custom, err } + if cluster.Spec.TLS.GetCertManagementPolicy() == v1beta1.CertManagementUserProvidedOnly { + secret := &corev1.Secret{ObjectMeta: naming.ReplicationClientCertSecret(cluster)} + if err := r.Client.Get(ctx, client.ObjectKeyFromObject(secret), secret); err != nil { + return nil, errors.Wrapf(err, "get user-provided replication TLS secret %s", secret.Name) + } + return secret, nil + } + certManagerManaged, err := r.isRootCACertManagerManaged(ctx, cluster) if err != nil { return nil, errors.Wrap(err, "failed to check if cert-manager manages root CA") @@ -409,7 +425,8 @@ func (r *Reconciler) reconcileInternalReplicationSecret( ) (*corev1.Secret, error) { existing := &corev1.Secret{ObjectMeta: naming.ReplicationClientCertSecret(cluster)} err := errors.WithStack(client.IgnoreNotFound( - r.Client.Get(ctx, client.ObjectKeyFromObject(existing), existing))) + r.Client.Get(ctx, client.ObjectKeyFromObject(existing), existing), + )) leaf := &pki.LeafCertificate{} commonName := postgres.ReplicationUser @@ -432,14 +449,16 @@ func (r *Reconciler) reconcileInternalReplicationSecret( // set labels and annotations intent.Annotations = naming.Merge( - cluster.Spec.Metadata.GetAnnotationsOrNil()) + cluster.Spec.Metadata.GetAnnotationsOrNil(), + ) intent.Labels = naming.Merge( cluster.Spec.Metadata.GetLabelsOrNil(), naming.WithPerconaLabels(map[string]string{ naming.LabelCluster: cluster.Name, naming.LabelClusterCertificate: "replication-client-tls", - }, cluster.Name, "", cluster.Labels[naming.LabelVersion])) + }, cluster.Name, "", cluster.Labels[naming.LabelVersion]), + ) // K8SPG-330: Keep this commented in case of conflicts. // We don't want to delete TLS secrets on cluster deletion. @@ -564,7 +583,8 @@ func (r *Reconciler) reconcilePatroniSwitchover(ctx context.Context, if len(targetInstance.Pods) != 1 { // We expect that a target instance should have one associated pod. return errors.Errorf( - "TargetInstance should have one pod. Pods (%d)", len(targetInstance.Pods)) + "TargetInstance should have one pod. Pods (%d)", len(targetInstance.Pods), + ) } } else { log.V(1).Info("TargetInstance not provided") diff --git a/internal/controller/postgrescluster/pgbackrest.go b/internal/controller/postgrescluster/pgbackrest.go index e37ff04d3..89b4d7a22 100644 --- a/internal/controller/postgrescluster/pgbackrest.go +++ b/internal/controller/postgrescluster/pgbackrest.go @@ -2224,7 +2224,15 @@ func (r *Reconciler) reconcilePGBackRestSecret(ctx context.Context, existing := &corev1.Secret{} err := errors.WithStack(client.IgnoreNotFound( - r.Client.Get(ctx, client.ObjectKeyFromObject(intent), existing))) + r.Client.Get(ctx, client.ObjectKeyFromObject(intent), existing), + )) + + if err == nil && cluster.Spec.TLS.GetCertManagementPolicy() == v1beta1.CertManagementUserProvidedOnly { + if repoHost != nil && len(existing.Name) == 0 { + return errors.Errorf("user-provided pgBackRest secret %q is missing", intent.Name) + } + return nil + } // K8SPG-330: Keep this commented in case of conflicts. // We don't want to delete TLS secrets on cluster deletion. diff --git a/internal/controller/postgrescluster/pgbouncer.go b/internal/controller/postgrescluster/pgbouncer.go index a2d585adf..0bac7b032 100644 --- a/internal/controller/postgrescluster/pgbouncer.go +++ b/internal/controller/postgrescluster/pgbouncer.go @@ -85,14 +85,16 @@ func (r *Reconciler) reconcilePGBouncerConfigMap( configmap.Annotations = naming.Merge( cluster.Spec.Metadata.GetAnnotationsOrNil(), - cluster.Spec.Proxy.PGBouncer.Metadata.GetAnnotationsOrNil()) + cluster.Spec.Proxy.PGBouncer.Metadata.GetAnnotationsOrNil(), + ) configmap.Labels = naming.Merge( cluster.Spec.Metadata.GetLabelsOrNil(), cluster.Spec.Proxy.PGBouncer.Metadata.GetLabelsOrNil(), naming.WithPerconaLabels(map[string]string{ naming.LabelCluster: cluster.Name, naming.LabelRole: naming.RolePGBouncer, - }, cluster.Name, "pgbouncer", cluster.Labels[naming.LabelVersion])) + }, cluster.Name, "pgbouncer", cluster.Labels[naming.LabelVersion]), + ) if err == nil { pgbouncer.ConfigMap(cluster, configmap) @@ -250,10 +252,12 @@ func (r *Reconciler) reconcilePGBouncerSecret( ) (*corev1.Secret, error) { existing := &corev1.Secret{ObjectMeta: naming.ClusterPGBouncer(cluster)} err := errors.WithStack( - r.Client.Get(ctx, client.ObjectKeyFromObject(existing), existing)) + r.Client.Get(ctx, client.ObjectKeyFromObject(existing), existing), + ) if client.IgnoreNotFound(err) != nil { return nil, err } + secretFound := err == nil if !cluster.Spec.Proxy.PGBouncerEnabled() { // PgBouncer is disabled; delete the Secret if it exists. @@ -265,6 +269,12 @@ func (r *Reconciler) reconcilePGBouncerSecret( err = client.IgnoreNotFound(err) + if cluster.Spec.TLS.GetCertManagementPolicy() == v1beta1.CertManagementUserProvidedOnly { + if !secretFound { + return nil, errors.Errorf("user-provided PgBouncer secret %q is missing", naming.ClusterPGBouncer(cluster).Name) + } + return existing, nil + } var userSecret *corev1.Secret if ref := cluster.Spec.Proxy.PGBouncer.UsersSecret; ref != nil && ref.Name != "" { userSecret = &corev1.Secret{} @@ -313,14 +323,16 @@ func (r *Reconciler) reconcilePGBouncerSecret( intent.Annotations = naming.Merge( cluster.Spec.Metadata.GetAnnotationsOrNil(), - cluster.Spec.Proxy.PGBouncer.Metadata.GetAnnotationsOrNil()) + cluster.Spec.Proxy.PGBouncer.Metadata.GetAnnotationsOrNil(), + ) intent.Labels = naming.Merge( cluster.Spec.Metadata.GetLabelsOrNil(), cluster.Spec.Proxy.PGBouncer.Metadata.GetLabelsOrNil(), naming.WithPerconaLabels(map[string]string{ naming.LabelCluster: cluster.Name, naming.LabelRole: naming.RolePGBouncer, - }, cluster.Name, "pgbouncer", cluster.Labels[naming.LabelVersion])) + }, cluster.Name, "pgbouncer", cluster.Labels[naming.LabelVersion]), + ) var additionalTrustedCAs [][]byte if err == nil { @@ -404,10 +416,12 @@ func (r *Reconciler) generatePGBouncerService( service.Annotations = naming.Merge( cluster.Spec.Metadata.GetAnnotationsOrNil(), - cluster.Spec.Proxy.PGBouncer.Metadata.GetAnnotationsOrNil()) + cluster.Spec.Proxy.PGBouncer.Metadata.GetAnnotationsOrNil(), + ) service.Labels = naming.Merge( cluster.Spec.Metadata.GetLabelsOrNil(), - cluster.Spec.Proxy.PGBouncer.Metadata.GetLabelsOrNil()) + cluster.Spec.Proxy.PGBouncer.Metadata.GetLabelsOrNil(), + ) if spec := cluster.Spec.Proxy.PGBouncer.Service; spec != nil { service.Annotations = naming.Merge(service.Annotations, @@ -511,14 +525,16 @@ func (r *Reconciler) generatePGBouncerDeployment( deploy.Annotations = naming.Merge( cluster.Spec.Metadata.GetAnnotationsOrNil(), - cluster.Spec.Proxy.PGBouncer.Metadata.GetAnnotationsOrNil()) + cluster.Spec.Proxy.PGBouncer.Metadata.GetAnnotationsOrNil(), + ) deploy.Labels = naming.Merge( cluster.Spec.Metadata.GetLabelsOrNil(), cluster.Spec.Proxy.PGBouncer.Metadata.GetLabelsOrNil(), naming.WithPerconaLabels(map[string]string{ naming.LabelCluster: cluster.Name, naming.LabelRole: naming.RolePGBouncer, - }, cluster.Name, "pgbouncer", cluster.Labels[naming.LabelVersion])) + }, cluster.Name, "pgbouncer", cluster.Labels[naming.LabelVersion]), + ) deploy.Spec.Selector = &metav1.LabelSelector{ MatchLabels: map[string]string{ naming.LabelCluster: cluster.Name, @@ -544,7 +560,8 @@ func (r *Reconciler) generatePGBouncerDeployment( naming.WithPerconaLabels(map[string]string{ naming.LabelCluster: cluster.Name, naming.LabelRole: naming.RolePGBouncer, - }, cluster.Name, "pgbouncer", cluster.Labels[naming.LabelVersion])) + }, cluster.Name, "pgbouncer", cluster.Labels[naming.LabelVersion]), + ) // if the shutdown flag is set, set pgBouncer replicas to 0 if cluster.Spec.Shutdown != nil && *cluster.Spec.Shutdown { @@ -575,7 +592,8 @@ func (r *Reconciler) generatePGBouncerDeployment( if !initialize.FromPointer(cluster.Spec.DisableDefaultPodScheduling) { deploy.Spec.Template.Spec.TopologySpreadConstraints = append( deploy.Spec.Template.Spec.TopologySpreadConstraints, - defaultTopologySpreadConstraints(*deploy.Spec.Selector)...) + defaultTopologySpreadConstraints(*deploy.Spec.Selector)..., + ) } // Restart containers any time they stop, die, are killed, etc. @@ -626,7 +644,8 @@ func (r *Reconciler) reconcilePGBouncerDeployment( configmap *corev1.ConfigMap, secret *corev1.Secret, ) error { deploy, specified, err := r.generatePGBouncerDeployment( - ctx, cluster, primaryCertificate, configmap, secret) + ctx, cluster, primaryCertificate, configmap, secret, + ) // Set observations whether the deployment exists or not. defer func() { diff --git a/internal/controller/postgrescluster/pki.go b/internal/controller/postgrescluster/pki.go index 36f79fd62..ea7fdf368 100644 --- a/internal/controller/postgrescluster/pki.go +++ b/internal/controller/postgrescluster/pki.go @@ -6,12 +6,14 @@ package postgrescluster import ( "context" + "strings" gover "github.com/hashicorp/go-version" "github.com/pkg/errors" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -29,6 +31,93 @@ const ( rootCertFile = "ca.crt" ) +// K8SPG-1045 +func (r *Reconciler) reconcileTLSCondition(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 + } + + var missing []string + + checkSecret := func(projection *corev1.SecretProjection, secretName string) error { + if projection != nil { + secretName = projection.Name + } + secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{ + Namespace: cluster.Namespace, + Name: secretName, + }} + err := r.Client.Get(ctx, client.ObjectKeyFromObject(secret), secret) + if client.IgnoreNotFound(err) != nil { + return errors.Wrapf(err, "get TLS secret %s", secret.Name) + } + if k8serrors.IsNotFound(err) { + missing = append(missing, secret.Name) + } + + return nil + } + + if err := checkSecret(cluster.Spec.CustomRootCATLSSecret, naming.PostgresRootCASecret(cluster).Name); err != nil { + return errors.Wrap(err, "check root ca secret") + } + if err := checkSecret(cluster.Spec.CustomTLSSecret, naming.PostgresTLSSecret(cluster).Name); err != nil { + return errors.Wrap(err, "check custom tls secret") + } + if err := checkSecret(cluster.Spec.CustomReplicationClientTLSSecret, naming.ReplicationClientCertSecret(cluster).Name); err != nil { + return errors.Wrap(err, "check replication client cert secret") + } + if err := checkSecret(nil, naming.PGBackRestSecret(cluster).Name); err != nil { + return errors.Wrap(err, "check pgBackRest TLS secret") + } + + if cluster.Spec.Proxy != nil && cluster.Spec.Proxy.PGBouncer != nil { + if err := checkSecret(nil, naming.ClusterPGBouncer(cluster).Name); err != nil { + return errors.Wrap(err, "check PgBouncer TLS secret") + } + } + + if cluster.Spec.CustomTLSSecret == nil { + instances := &appsv1.StatefulSetList{} + if err := r.Client.List( + ctx, instances, + client.InNamespace(cluster.Namespace), + client.MatchingLabels{ + naming.LabelCluster: cluster.Name, + naming.LabelData: naming.DataPostgres, + }, + ); err != nil { + return errors.Wrap(err, "list instances to check TLS secrets") + } + + for i := range instances.Items { + if err := checkSecret(nil, naming.InstanceCertificates(&instances.Items[i]).Name); err != nil { + return errors.Wrap(err, "check instance TLS secret") + } + } + } + + if len(missing) > 0 { + cond.Message = "Missing user-provided TLS secrets: " + strings.Join(missing, ", ") + ". certManagementPolicy is userProvidedOnly" + cond.Reason = "TLSSecretsMissing" + cond.Status = metav1.ConditionFalse + meta.SetStatusCondition(&cluster.Status.Conditions, cond) + return nil + } + + meta.SetStatusCondition(&cluster.Status.Conditions, cond) + return nil +} + // +kubebuilder:rbac:groups="",resources="secrets",verbs={get} // +kubebuilder:rbac:groups="",resources="secrets",verbs={create,patch} @@ -89,6 +178,23 @@ func (r *Reconciler) reconcileRootCertificate( } } + if cluster.Spec.TLS.GetCertManagementPolicy() == v1beta1.CertManagementUserProvidedOnly { + if err != nil { + return nil, errors.Wrap(err, "get user-provided root CA secret") + } + + root := &pki.RootCertificateAuthority{} + if err := root.Certificate.UnmarshalText(existing.Data[certificateKey]); err != nil { + return nil, errors.Wrapf(err, "parse certificate in user-provided root CA secret %q", existing.Name) + } + if err := root.PrivateKey.UnmarshalText(existing.Data[privateKey]); err != nil { + return nil, errors.Wrapf(err, "parse private key in user-provided root CA secret %q", existing.Name) + } + if !pki.RootIsValid(root) { + return nil, errors.Errorf("user-provided root CA secret %q is invalid", existing.Name) + } + return root, nil + } // If the secret is managed by cert-manager, parse it using cert-manager key names // (tls.crt/tls.key) and return without overwriting the secret with internal PKI. if err == nil && existing.Annotations["cert-manager.io/certificate-name"] != "" { @@ -235,6 +341,9 @@ func (r *Reconciler) reconcileClusterCertificate( return cluster.Spec.CustomTLSSecret, nil } + if cluster.Spec.TLS.GetCertManagementPolicy() == v1beta1.CertManagementUserProvidedOnly { + return r.reconcileUserProvidedClusterCertificate(ctx, cluster) + } certManagerManaged, err := r.isRootCACertManagerManaged(ctx, cluster) if err != nil { return nil, errors.Wrap(err, "failed to check if cert-manager manages root CA") @@ -257,6 +366,23 @@ func (r *Reconciler) reconcileClusterCertificate( return r.reconcileInternalClusterCertificate(ctx, root, cluster, primaryService, replicaService) } +func (r *Reconciler) reconcileUserProvidedClusterCertificate( + ctx context.Context, cluster *v1beta1.PostgresCluster, +) (*corev1.SecretProjection, error) { + secret := &corev1.Secret{ObjectMeta: naming.PostgresTLSSecret(cluster)} + if err := r.Client.Get(ctx, client.ObjectKeyFromObject(secret), secret); err != nil { + return nil, errors.Wrapf(err, "get user-provided TLS secret %s", secret.Name) + } + + for _, key := range []string{clusterCertFile, clusterKeyFile, rootCertFile} { + if len(secret.Data[key]) == 0 { + return nil, errors.Errorf("user-provided TLS secret %q is missing key %q", secret.Name, key) + } + } + + return clusterCertSecretProjection(secret), nil +} + // reconcileInternalClusterCertificate creates a cluster certificate using internal PKI. func (r *Reconciler) reconcileInternalClusterCertificate( ctx context.Context, root *pki.RootCertificateAuthority, @@ -269,7 +395,8 @@ func (r *Reconciler) reconcileInternalClusterCertificate( existing := &corev1.Secret{ObjectMeta: naming.PostgresTLSSecret(cluster)} err := errors.WithStack(client.IgnoreNotFound( - r.Client.Get(ctx, client.ObjectKeyFromObject(existing), existing))) + r.Client.Get(ctx, client.ObjectKeyFromObject(existing), existing), + )) leaf := &pki.LeafCertificate{} primaryServiceDNSNames, err := naming.ServiceDNSNames(ctx, primaryService, cluster.Spec.ClusterServiceDNSSuffix) @@ -307,7 +434,8 @@ func (r *Reconciler) reconcileInternalClusterCertificate( naming.WithPerconaLabels(map[string]string{ naming.LabelCluster: cluster.Name, naming.LabelClusterCertificate: "postgres-tls", - }, cluster.Name, "", cluster.Labels[naming.LabelVersion])) + }, cluster.Name, "", cluster.Labels[naming.LabelVersion]), + ) // K8SPG-330: Keep this commented in case of conflicts. // We don't want to delete TLS secrets on cluster deletion. diff --git a/internal/controller/postgrescluster/pki_test.go b/internal/controller/postgrescluster/pki_test.go index 1b9e4d7a7..e4b8109c4 100644 --- a/internal/controller/postgrescluster/pki_test.go +++ b/internal/controller/postgrescluster/pki_test.go @@ -18,6 +18,7 @@ import ( "gotest.tools/v3/assert" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -32,6 +33,113 @@ import ( "github.com/percona/percona-postgresql-operator/v2/pkg/apis/upstream.pgv2.percona.com/v1beta1" ) +func TestReconcileTLSCondition(t *testing.T) { + condition := func(t *testing.T, cluster *v1beta1.PostgresCluster, status metav1.ConditionStatus) *metav1.Condition { + t.Helper() + assert.Assert(t, meta.IsStatusConditionPresentAndEqual( + cluster.Status.Conditions, v1beta1.ConditionTypeTLSSecretsReady, status, + )) + condition := meta.FindStatusCondition(cluster.Status.Conditions, v1beta1.ConditionTypeTLSSecretsReady) + assert.Assert(t, condition != nil) + return condition + } + + t.Run("automatic certificate management", func(t *testing.T) { + cluster := testCluster() + cluster.Generation = 7 + cluster.Spec.TLS = nil + cluster.Status.Conditions = []metav1.Condition{{ + Type: v1beta1.ConditionTypeTLSSecretsReady, + Status: metav1.ConditionFalse, + }} + + r := &Reconciler{} + assert.NilError(t, r.reconcileTLSCondition(t.Context(), cluster)) + + condition := condition(t, cluster, metav1.ConditionTrue) + assert.Equal(t, condition.Reason, "TLSSecretsFound") + assert.Equal(t, condition.Message, "certManagementPolicy is auto") + assert.Equal(t, condition.ObservedGeneration, int64(7)) + }) + + t.Run("missing user-provided secrets", func(t *testing.T) { + cluster := testCluster() + cluster.Namespace = "postgres-operator" + cluster.Generation = 11 + cluster.Spec.TLS = &v1beta1.TLSSpec{ + CertManagementPolicy: v1beta1.CertManagementUserProvidedOnly, + } + cluster.Spec.CustomRootCATLSSecret = &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{Name: "custom-root-ca"}, + } + cluster.Spec.CustomReplicationClientTLSSecret = &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{Name: "custom-replication"}, + } + + instance := &appsv1.StatefulSet{ObjectMeta: metav1.ObjectMeta{ + Name: "hippo-instance1-abcd", + Namespace: cluster.Namespace, + Labels: map[string]string{ + naming.LabelCluster: cluster.Name, + naming.LabelData: naming.DataPostgres, + }, + }} + instanceWithoutMatchingLabels := &appsv1.StatefulSet{ObjectMeta: metav1.ObjectMeta{ + Name: "unrelated-instance", + Namespace: cluster.Namespace, + }} + + r := &Reconciler{Client: fake.NewClientBuilder().WithObjects(instance, instanceWithoutMatchingLabels).Build()} + assert.NilError(t, r.reconcileTLSCondition(t.Context(), cluster)) + + condition := condition(t, cluster, metav1.ConditionFalse) + assert.Equal(t, condition.Reason, "TLSSecretsMissing") + assert.Equal(t, condition.ObservedGeneration, int64(11)) + assert.Equal(t, condition.Message, "Missing user-provided TLS secrets: "+strings.Join([]string{ + "custom-root-ca", + naming.PostgresTLSSecret(cluster).Name, + "custom-replication", + naming.PGBackRestSecret(cluster).Name, + naming.ClusterPGBouncer(cluster).Name, + naming.InstanceCertificates(instance).Name, + }, ", ")+". certManagementPolicy is userProvidedOnly") + }) + + t.Run("user-provided secrets found", func(t *testing.T) { + cluster := testCluster() + cluster.Namespace = "postgres-operator" + cluster.Generation = 13 + cluster.Spec.TLS = &v1beta1.TLSSpec{ + CertManagementPolicy: v1beta1.CertManagementUserProvidedOnly, + } + cluster.Spec.CustomRootCATLSSecret = &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{Name: "custom-root-ca"}, + } + cluster.Spec.CustomTLSSecret = &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{Name: "custom-postgres-tls"}, + } + cluster.Spec.CustomReplicationClientTLSSecret = &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{Name: "custom-replication"}, + } + + objects := []client.Object{ + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "custom-root-ca", Namespace: cluster.Namespace}}, + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "custom-postgres-tls", Namespace: cluster.Namespace}}, + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "custom-replication", Namespace: cluster.Namespace}}, + &corev1.Secret{ObjectMeta: naming.PGBackRestSecret(cluster)}, + &corev1.Secret{ObjectMeta: naming.ClusterPGBouncer(cluster)}, + } + + r := &Reconciler{Client: fake.NewClientBuilder().WithObjects(objects...).Build()} + assert.NilError(t, r.reconcileTLSCondition(t.Context(), cluster)) + + condition := condition(t, cluster, metav1.ConditionTrue) + assert.Equal(t, condition.Reason, "TLSSecretsFound") + assert.Equal(t, condition.Message, "") + assert.Equal(t, condition.ObservedGeneration, int64(13)) + }) +} + // TestReconcileCerts tests the proper reconciliation of the root ca certificate // secret, leaf certificate secrets and the updates that occur when updates are // made to the cluster certificates generally. For the removal of ownership diff --git a/internal/controller/postgrescluster/watches.go b/internal/controller/postgrescluster/watches.go index a9b8822d5..f6f3a5953 100644 --- a/internal/controller/postgrescluster/watches.go +++ b/internal/controller/postgrescluster/watches.go @@ -19,6 +19,23 @@ import ( "github.com/percona/percona-postgresql-operator/v2/pkg/apis/upstream.pgv2.percona.com/v1beta1" ) +// 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 { + return handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { + cluster := obj.GetLabels()[naming.LabelCluster] + if len(cluster) > 0 { + return []reconcile.Request{ + {NamespacedName: client.ObjectKey{ + Namespace: obj.GetNamespace(), + Name: cluster, + }}, + } + } + return nil + }) +} + // watchCertManagerSecrets returns a handler.EventHandler for cert-manager-issued // Secrets. These Secrets are owned by Certificate resources (not PostgresCluster), // so they are not covered by Owns(&corev1.Secret{}). diff --git a/pkg/apis/upstream.pgv2.percona.com/v1beta1/postgrescluster_types.go b/pkg/apis/upstream.pgv2.percona.com/v1beta1/postgrescluster_types.go index a136fee95..2db06daeb 100644 --- a/pkg/apis/upstream.pgv2.percona.com/v1beta1/postgrescluster_types.go +++ b/pkg/apis/upstream.pgv2.percona.com/v1beta1/postgrescluster_types.go @@ -292,10 +292,31 @@ type TLSSpec struct { CAValidityDuration *metav1.Duration `json:"caValidityDuration,omitempty"` // +optional PGBackRestCertValidityDuration *metav1.Duration `json:"pgBackRestCertValidityDuration,omitempty"` + // +kubebuilder:default=auto + // +kubebuilder:validation:Enum={auto,userProvidedOnly} + CertManagementPolicy CertManagementPolicy `json:"certManagementPolicy,omitempty"` // +optional IssuerConf *cmmeta.IssuerReference `json:"issuerConf,omitempty"` } +func (s *TLSSpec) GetCertManagementPolicy() CertManagementPolicy { + if s == nil || s.CertManagementPolicy == "" { + return CertManagementAuto + } + return s.CertManagementPolicy +} + +type CertManagementPolicy string + +const ( + CertManagementAuto CertManagementPolicy = "auto" + CertManagementUserProvidedOnly CertManagementPolicy = "userProvidedOnly" +) + +const ( + ConditionTypeTLSSecretsReady = "TLSSecretsReady" +) + // DataSource defines data sources for a new PostgresCluster. type DataSource struct { // Defines a pgBackRest cloud-based data source that can be used to pre-populate the