Skip to content
54 changes: 16 additions & 38 deletions internal/controller/postgrescluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"github.com/percona/percona-postgresql-operator/v2/internal/initialize"
"github.com/percona/percona-postgresql-operator/v2/internal/naming"
"github.com/percona/percona-postgresql-operator/v2/internal/patroni"
"github.com/percona/percona-postgresql-operator/v2/internal/patroni/dcs"
"github.com/percona/percona-postgresql-operator/v2/internal/pki"
"github.com/percona/percona-postgresql-operator/v2/internal/postgres"
"github.com/percona/percona-postgresql-operator/v2/internal/util"
Expand Down Expand Up @@ -45,7 +46,7 @@

if err == nil {
err = patroni.ClusterConfigMap(ctx, cluster, pgHBAs, pgParameters,
clusterConfigMap)
dcs.For(cluster).ClusterYAML(cluster), clusterConfigMap)
}
if err == nil {
err = errors.WithStack(r.apply(ctx, clusterConfigMap))
Expand Down Expand Up @@ -118,43 +119,20 @@

err := errors.WithStack(r.setControllerReference(cluster, service))

// Endpoints for a Service have the same name as the Service. Copy labels,
// annotations, and ownership, too.
endpoints := &corev1.Endpoints{}
service.ObjectMeta.DeepCopyInto(&endpoints.ObjectMeta)
endpoints.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Endpoints"))

if leader == nil {
// TODO(cbandy): We need to build a different kind of Service here.
return nil, nil, errors.New("Patroni DCS other than Kubernetes Endpoints is not implemented")
spec, subset, backendErr := dcs.For(cluster).PrimaryService(cluster, leader)
if backendErr != nil {
return nil, nil, backendErr
}

// Allocate no IP address (headless) and manage the Endpoints ourselves.
// - https://docs.k8s.io/concepts/services-networking/service/#headless-services
// - https://docs.k8s.io/concepts/services-networking/service/#services-without-selectors
service.Spec.ClusterIP = corev1.ClusterIPNone
service.Spec.Selector = nil

service.Spec.Ports = []corev1.ServicePort{{
Name: naming.PortPostgreSQL,
Port: *cluster.Spec.Port,
Protocol: corev1.ProtocolTCP,
TargetPort: intstr.FromString(naming.PortPostgreSQL),
}}

// Resolve to the ClusterIP for which Patroni has configured the Endpoints.
endpoints.Subsets = []corev1.EndpointSubset{{
Addresses: []corev1.EndpointAddress{{IP: leader.Spec.ClusterIP}},
}}

// Copy the EndpointPorts from the ServicePorts.
for _, sp := range service.Spec.Ports {
endpoints.Subsets[0].Ports = append(endpoints.Subsets[0].Ports,
corev1.EndpointPort{
Name: sp.Name,
Port: sp.Port,
Protocol: sp.Protocol,
})
service.Spec = spec

var endpoints *corev1.Endpoints

Check failure on line 128 in internal/controller/postgrescluster/cluster.go

View workflow job for this annotation

GitHub Actions / runner / suggester / golangci-lint

SA1019: corev1.Endpoints is deprecated: This API is deprecated in v1.33+. Use discoveryv1.EndpointSlice. (staticcheck)
if subset != nil {
// Endpoints for a Service have the same name as the Service. Copy labels,
// annotations, and ownership, too.
endpoints = &corev1.Endpoints{}

Check failure on line 132 in internal/controller/postgrescluster/cluster.go

View workflow job for this annotation

GitHub Actions / runner / suggester / golangci-lint

SA1019: corev1.Endpoints is deprecated: This API is deprecated in v1.33+. Use discoveryv1.EndpointSlice. (staticcheck)
service.ObjectMeta.DeepCopyInto(&endpoints.ObjectMeta)
endpoints.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Endpoints"))
endpoints.Subsets = []corev1.EndpointSubset{*subset}

Check failure on line 135 in internal/controller/postgrescluster/cluster.go

View workflow job for this annotation

GitHub Actions / runner / suggester / golangci-lint

SA1019: corev1.EndpointSubset is deprecated: This API is deprecated in v1.33+. (staticcheck)
}

return service, endpoints, err
Expand All @@ -178,7 +156,7 @@
if err == nil {
err = errors.WithStack(r.apply(ctx, service))
}
if err == nil {
if err == nil && endpoints != nil {
err = errors.WithStack(r.apply(ctx, endpoints))
}
return service, err
Expand Down
4 changes: 2 additions & 2 deletions internal/controller/postgrescluster/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ func TestGenerateClusterPrimaryService(t *testing.T) {
leader.Spec.ClusterIP = "1.9.8.3"

_, _, err := reconciler.generateClusterPrimaryService(cluster, nil)
assert.ErrorContains(t, err, "not implemented")
assert.ErrorContains(t, err, "not available yet")

alwaysExpect := func(t testing.TB, service *corev1.Service, endpoints *corev1.Endpoints) {
assert.Assert(t, cmp.MarshalMatches(service.TypeMeta, `
Expand Down Expand Up @@ -700,7 +700,7 @@ func TestReconcileClusterPrimaryService(t *testing.T) {
assert.NilError(t, cc.Create(ctx, cluster))

_, err := reconciler.reconcileClusterPrimaryService(ctx, cluster, nil)
assert.ErrorContains(t, err, "not implemented")
assert.ErrorContains(t, err, "not available yet")

leader := &corev1.Service{}
leader.Spec.ClusterIP = "192.0.2.10"
Expand Down
9 changes: 6 additions & 3 deletions internal/controller/postgrescluster/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/percona/percona-postgresql-operator/v2/internal/logging"
"github.com/percona/percona-postgresql-operator/v2/internal/naming"
"github.com/percona/percona-postgresql-operator/v2/internal/patroni"
"github.com/percona/percona-postgresql-operator/v2/internal/patroni/dcs"
"github.com/percona/percona-postgresql-operator/v2/internal/pgbackrest"
"github.com/percona/percona-postgresql-operator/v2/internal/pgtde"
"github.com/percona/percona-postgresql-operator/v2/internal/pki"
Expand Down Expand Up @@ -1296,13 +1297,15 @@ func (r *Reconciler) reconcileInstance(
}

// K8SPG-708
initImage, err := k8s.InitImage(ctx, r.Client, cluster, spec)
var initImage string
initImage, err = k8s.InitImage(ctx, r.Client, cluster, spec)
if err != nil {
return errors.Wrap(err, "failed to determine initial init image")
}

dcsEnvVars := dcs.For(cluster).InstanceEnvVars(cluster, patroniLeaderService, instance.Spec.Template.Spec.Containers)
err = patroni.InstancePod(
ctx, cluster, clusterConfigMap, clusterPodService, patroniLeaderService,
ctx, cluster, clusterConfigMap, clusterPodService, dcsEnvVars,
spec, instanceCertificates, instanceConfigMap, &instance.Spec.Template, initImage) // K8SPG-708
}

Expand Down Expand Up @@ -1572,7 +1575,7 @@ func (r *Reconciler) reconcileInstanceConfigMap(
}, cluster.Name, "pg", cluster.Labels[naming.LabelVersion]))

if err == nil {
err = patroni.InstanceConfigMap(ctx, cluster, spec, instanceConfigMap)
err = patroni.InstanceConfigMap(ctx, cluster, spec, dcs.For(cluster).InstanceYAML(cluster), instanceConfigMap)
}
if err == nil {
err = errors.WithStack(r.apply(ctx, instanceConfigMap))
Expand Down
154 changes: 18 additions & 136 deletions internal/controller/postgrescluster/patroni.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ import (
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/percona/percona-postgresql-operator/v2/internal/initialize"
"github.com/percona/percona-postgresql-operator/v2/internal/logging"
"github.com/percona/percona-postgresql-operator/v2/internal/naming"
"github.com/percona/percona-postgresql-operator/v2/internal/patroni"
"github.com/percona/percona-postgresql-operator/v2/internal/patroni/dcs"
"github.com/percona/percona-postgresql-operator/v2/internal/pki"
"github.com/percona/percona-postgresql-operator/v2/internal/postgres"
"github.com/percona/percona-postgresql-operator/v2/percona/certmanager"
Expand All @@ -30,22 +29,7 @@ import (
func (r *Reconciler) deletePatroniArtifacts(
ctx context.Context, cluster *v1beta1.PostgresCluster,
) error {
// TODO(cbandy): This could also be accomplished by adopting the Endpoints
// as Patroni creates them. Would their events cause too many reconciles?
// Foreground deletion may force us to adopt and set finalizers anyway.

selector, err := naming.AsSelector(naming.ClusterPatronis(cluster))
if err == nil {
err = errors.WithStack(
r.Client.DeleteAllOf(
ctx, &corev1.Endpoints{},
client.InNamespace(cluster.Namespace),
client.MatchingLabelsSelector{Selector: selector},
),
)
}

return err
return dcs.For(cluster).Delete(ctx, r.Client, cluster)
}

func (r *Reconciler) handlePatroniRestarts(
Expand Down Expand Up @@ -147,15 +131,12 @@ func (r *Reconciler) handlePatroniRestarts(
func (r *Reconciler) reconcilePatroniDistributedConfiguration(
ctx context.Context, cluster *v1beta1.PostgresCluster,
) error {
// When using Endpoints for DCS, Patroni needs a Service to ensure that the
// Endpoints object is not removed by Kubernetes at startup. Patroni will
// create this object if it has permission to do so, but it won't set any
// ownership.
// - https://releases.k8s.io/v1.16.0/pkg/controller/endpoint/endpoints_controller.go#L547
// - https://releases.k8s.io/v1.20.0/pkg/controller/endpoint/endpoints_controller.go#L580
// - https://github.com/zalando/patroni/blob/v2.0.1/patroni/dcs/kubernetes.py#L865-L881
dcsService := &corev1.Service{ObjectMeta: naming.PatroniDistributedConfiguration(cluster)}
dcsService.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Service"))
// The DCS backend may not own any Kubernetes object for its distributed
// configuration (e.g. an external DCS).
dcsService := dcs.For(cluster).DistributedConfigurationService(cluster)
if dcsService == nil {
return nil
}

err := errors.WithStack(r.setControllerReference(cluster, dcsService))

Expand All @@ -170,11 +151,6 @@ func (r *Reconciler) reconcilePatroniDistributedConfiguration(
}, 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
dcsService.Spec.ClusterIP = corev1.ClusterIPNone
dcsService.Spec.Selector = nil

if err == nil {
err = errors.WithStack(r.apply(ctx, dcsService))
}
Expand Down Expand Up @@ -233,80 +209,6 @@ func (r *Reconciler) reconcilePatroniDynamicConfiguration(
)
}

// generatePatroniLeaderLeaseService returns a v1.Service that exposes the
// Patroni leader when Patroni is using Endpoints for its leader elections.
func (r *Reconciler) generatePatroniLeaderLeaseService(
cluster *v1beta1.PostgresCluster) (*corev1.Service, error,
) {
service := &corev1.Service{ObjectMeta: naming.PatroniLeaderEndpoints(cluster)}
service.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Service"))

service.Annotations = naming.Merge(
cluster.Spec.Metadata.GetAnnotationsOrNil(),
)
service.Labels = naming.Merge(
cluster.Spec.Metadata.GetLabelsOrNil(),
)

if spec := cluster.Spec.Service; spec != nil {
service.Annotations = naming.Merge(service.Annotations,
spec.Metadata.GetAnnotationsOrNil())
service.Labels = naming.Merge(service.Labels,
spec.Metadata.GetLabelsOrNil())
}

// add our labels last so they aren't overwritten
service.Labels = naming.Merge(service.Labels,
naming.WithPerconaLabels(map[string]string{ // K8SPG-430
naming.LabelCluster: cluster.Name,
naming.LabelPatroni: naming.PatroniScope(cluster),
}, cluster.Name, "", cluster.Labels[naming.LabelVersion]))

// Allocate an IP address and/or node port and let Patroni manage the Endpoints.
// Patroni will ensure that they always route to the elected leader.
// - https://docs.k8s.io/concepts/services-networking/service/#services-without-selectors
service.Spec.Selector = nil

// The TargetPort must be the name (not the number) of the PostgreSQL
// ContainerPort. This name allows the port number to differ between
// instances, which can happen during a rolling update.
servicePort := corev1.ServicePort{
Name: naming.PortPostgreSQL,
Port: *cluster.Spec.Port,
Protocol: corev1.ProtocolTCP,
TargetPort: intstr.FromString(naming.PortPostgreSQL),
}

if spec := cluster.Spec.Service; spec == nil {
service.Spec.Type = corev1.ServiceTypeClusterIP
} else {
service.Spec.Type = corev1.ServiceType(spec.Type)
// K8SPG-389
service.Spec.LoadBalancerSourceRanges = spec.LoadBalancerSourceRanges

if spec.NodePort != nil {
if service.Spec.Type == corev1.ServiceTypeClusterIP {
// The NodePort can only be set when the Service type is NodePort or
// LoadBalancer. However, due to a known issue prior to Kubernetes
// 1.20, we clear these errors during our apply. To preserve the
// appropriate behavior, we log an Event and return an error.
// TODO(tjmoore4): Once Validation Rules are available, this check
// and event could potentially be removed in favor of that validation
r.Recorder.Eventf(cluster, corev1.EventTypeWarning, "MisconfiguredClusterIP",
"NodePort cannot be set with type ClusterIP on Service %q", service.Name)
return nil, errors.Errorf("NodePort cannot be set with type ClusterIP on Service %q", service.Name)
}
servicePort.NodePort = *spec.NodePort
}
service.Spec.ExternalTrafficPolicy = initialize.FromPointer(spec.ExternalTrafficPolicy)
service.Spec.InternalTrafficPolicy = spec.InternalTrafficPolicy
}
service.Spec.Ports = []corev1.ServicePort{servicePort}

err := errors.WithStack(r.setControllerReference(cluster, service))
return service, err
}

// +kubebuilder:rbac:groups="",resources="services",verbs={create,patch}

// reconcilePatroniLeaderLease sets labels and ownership on the objects Patroni
Expand All @@ -315,12 +217,11 @@ func (r *Reconciler) generatePatroniLeaderLeaseService(
func (r *Reconciler) reconcilePatroniLeaderLease(
ctx context.Context, cluster *v1beta1.PostgresCluster,
) (*corev1.Service, error) {
// When using Endpoints for DCS, Patroni needs a Service to ensure that the
// Endpoints object is not removed by Kubernetes at startup.
// - https://releases.k8s.io/v1.16.0/pkg/controller/endpoint/endpoints_controller.go#L547
// - https://releases.k8s.io/v1.20.0/pkg/controller/endpoint/endpoints_controller.go#L580
service, err := r.generatePatroniLeaderLeaseService(cluster)
if err == nil {
service, err := dcs.For(cluster).LeaderLeaseService(cluster, r.Recorder)
if err == nil && service != nil {
err = errors.WithStack(r.setControllerReference(cluster, service))
}
if err == nil && service != nil {
err = errors.WithStack(r.apply(ctx, service))
}
return service, err
Expand All @@ -333,39 +234,20 @@ func (r *Reconciler) reconcilePatroniStatus(
ctx context.Context, cluster *v1beta1.PostgresCluster,
observedInstances *observedInstances,
) (time.Duration, error) {
var requeue time.Duration
log := logging.FromContext(ctx)

var readyInstance bool
for _, instance := range observedInstances.forCluster {
if r, _ := instance.IsReady(); r {
readyInstance = true
}
}

dcs := &corev1.Endpoints{ObjectMeta: naming.PatroniDistributedConfiguration(cluster)}
err := errors.WithStack(client.IgnoreNotFound(
r.Client.Get(ctx, client.ObjectKeyFromObject(dcs), dcs),
))

if err == nil {
if dcs.Annotations["initialize"] != "" {
// After bootstrap, Patroni writes the cluster system identifier to DCS.
cluster.Status.Patroni.SystemIdentifier = dcs.Annotations["initialize"]
} else if readyInstance {
// While we typically expect a value for the initialize key to be present in the
// Endpoints above by the time the StatefulSet for any instance indicates "ready"
// (since Patroni writes this value after successful cluster bootstrap, at which time
// the initial primary should transition to "ready"), sometimes this is not the case
// and the "initialize" key is not yet present. Therefore, if a "ready" instance
// is detected in the cluster we assume this is the case, and simply log a message and
// requeue in order to try again until the expected value is found.
log.Info("detected ready instance but no initialize value")
requeue = time.Second
}
observation, err := dcs.For(cluster).Observe(ctx, r.Client, cluster, readyInstance)
if err == nil && observation.SystemIdentifier != "" {
// After bootstrap, the DCS backend reports the cluster system identifier.
cluster.Status.Patroni.SystemIdentifier = observation.SystemIdentifier
}

return requeue, err
return observation.RequeueAfter, err
}

// reconcileReplicationSecret creates a secret containing the TLS
Expand Down
Loading
Loading