Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions internal/controller/postgrescluster/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,11 @@ func (r *Reconciler) Reconcile(
if err == nil {
err = r.reconcilePostgresDatabases(ctx, cluster, instances, patchClusterStatus)
}
// K8SPG-911: the two reconcilers around this one need a writable instance.
// A standby has none, so its pg_tde status comes from what it reports.
if err == nil {
r.reconcilePGTDEStandby(ctx, cluster, instances)
}
if err == nil {
err = r.reconcilePGTDEProviders(ctx, cluster, instances, patchClusterStatus)
}
Expand Down
24 changes: 24 additions & 0 deletions internal/controller/postgrescluster/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,30 @@ func (observed *observedInstances) writablePod(container string) (*corev1.Pod, *
return nil, nil
}

// standbyLeaderPod finds the instance Patroni reports as the standby leader:
// the one replaying from a source outside this cluster. Unlike writablePod it
// deliberately accepts an instance in recovery, so callers must send it only
// statements that read.
func (observed *observedInstances) standbyLeaderPod(container string) (*corev1.Pod, *Instance) {
if observed == nil {
return nil, nil
}

for _, instance := range observed.forCluster {
if terminating, known := instance.IsTerminating(); terminating || !known {
continue
}
if len(instance.Pods) != 1 || !patroni.PodIsStandbyLeader(instance.Pods[0]) {
continue
}
if running, known := instance.IsRunning(container); running && known {
return instance.Pods[0], instance
}
}

return nil, nil
}

// runningPods returns the Pod of every non-terminating instance whose named
// container is running, and whether that accounts for every instance in the
// cluster. Callers that must reach the whole cluster, rather than any one
Expand Down
82 changes: 82 additions & 0 deletions internal/controller/postgrescluster/instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,88 @@ func TestWritablePod(t *testing.T) {
})
}

// K8SPG-911
func TestStandbyLeaderPod(t *testing.T) {
container := "container"

instance := func(role string, terminating, running bool) *Instance {
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "namespace",
Name: "pod",
},
Status: corev1.PodStatus{
ContainerStatuses: []corev1.ContainerStatus{{Name: container}},
},
}
if role != "" {
pod.Annotations = map[string]string{"status": `{"role":"` + role + `"}`}
}
if terminating {
pod.DeletionTimestamp = &metav1.Time{}
}
if running {
pod.Status.ContainerStatuses[0].State.Running = new(corev1.ContainerStateRunning)
} else {
pod.Status.ContainerStatuses[0].State.Waiting = new(corev1.ContainerStateWaiting)
}

return &Instance{Name: "instance", Pods: []*corev1.Pod{pod}, Runner: &appsv1.StatefulSet{}}
}

t.Run("empty observed", func(t *testing.T) {
pod, instance := (&observedInstances{}).standbyLeaderPod(container)
assert.Assert(t, pod == nil)
assert.Assert(t, instance == nil)
})

t.Run("nil observed", func(t *testing.T) {
var observed *observedInstances
pod, instance := observed.standbyLeaderPod(container)
assert.Assert(t, pod == nil)
assert.Assert(t, instance == nil)
})

for _, tc := range []struct {
name string
role string
terminating bool
running bool
expected bool
}{
{name: "StandbyLeader", role: "standby_leader", running: true, expected: true},
{name: "Terminating", role: "standby_leader", terminating: true, running: true},
{name: "NotRunning", role: "standby_leader"},
{name: "Replica", role: "replica", running: true},
{name: "NoStatusAnnotation", running: true},
{
// The role label Patroni puts on a standby leader is the same one it
// puts on a real primary, which is why standbyLeaderPod reads the
// member status instead. A writable instance belongs to
// writablePod, and the two must never both match.
name: "Primary", role: "primary", running: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
inst := instance(tc.role, tc.terminating, tc.running)
observed := &observedInstances{forCluster: []*Instance{inst}}

pod, matched := observed.standbyLeaderPod(container)
if !tc.expected {
assert.Assert(t, pod == nil)
assert.Assert(t, matched == nil)
return
}

assert.Assert(t, pod != nil)
assert.Equal(t, matched, inst)

writable, _ := observed.writablePod(container)
assert.Assert(t, writable == nil, "a standby leader is not writable")
})
}
}

func TestAddPGBackRestToInstancePodSpec(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading