Skip to content

Commit 16072d8

Browse files
phiscoarmruleonardocegbartolinimnencia
authored
feat: instance fencing through annotation
It's now possible to fence one or more instances in the cluster by setting the content of the "k8s.enterprisedb.io/fencedInstances" annotation to a JSON list of instances (e.g. ["cluster-example-1","cluster-example-2"]) or using the "kubectl cnp fence on/off" subcommand. Currently, fencing an instance will result in: - switchovers/failovers are not to happen while at least an instance in the cluster is fenced - fenced instances will be shut down, but the pod will be kept running, but not considered ready, Postgres will be restarted once the fencing is removed for that specific instance - in case a primary instance is fenced, as soon as the fencing is removed a failover to another instance will be performed Co-authored-by: Armando Ruocco <armando.ruocco@enterprisedb.com> Co-authored-by: Leonardo Cecchi <leonardo.cecchi@enterprisedb.com> Co-authored-by: Gabriele Bartolini <gabriele.bartolini@enterprisedb.com> Co-authored-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
1 parent 1691608 commit 16072d8

File tree

28 files changed

+1220
-146
lines changed

28 files changed

+1220
-146
lines changed

api/v1/cluster_types.go

+13
Original file line numberDiff line numberDiff line change
@@ -1316,6 +1316,19 @@ func (cluster *Cluster) IsReusePVCEnabled() bool {
13161316
return reusePVC
13171317
}
13181318

1319+
// IsInstanceFenced check if in a given instance should be fenced
1320+
func (cluster *Cluster) IsInstanceFenced(instance string) bool {
1321+
fencedInstances, err := utils.GetFencedInstances(cluster.Annotations)
1322+
if err != nil {
1323+
return false
1324+
}
1325+
1326+
if fencedInstances.Has(utils.FenceAllServers) {
1327+
return true
1328+
}
1329+
return fencedInstances.Has(instance)
1330+
}
1331+
13191332
// ShouldResizeInUseVolumes is true when we should resize PVC we already
13201333
// created
13211334
func (cluster *Cluster) ShouldResizeInUseVolumes() bool {

api/v1/cluster_types_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ package v1
99
import (
1010
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1111

12+
"github.com/EnterpriseDB/cloud-native-postgresql/pkg/utils"
13+
1214
. "github.com/onsi/ginkgo/v2"
1315
. "github.com/onsi/gomega"
1416
)
@@ -584,3 +586,51 @@ var _ = Describe("Barman Endpoint CA for replica cluster", func() {
584586
Expect(cluster3.GetBarmanEndpointCAForReplicaCluster()).To(Not(BeNil()))
585587
})
586588
})
589+
590+
var _ = Describe("Fencing annotation", func() {
591+
When("one instance is fenced", func() {
592+
cluster := Cluster{
593+
ObjectMeta: v1.ObjectMeta{
594+
Annotations: map[string]string{
595+
utils.FencedInstanceAnnotation: "[\"one\"]",
596+
},
597+
},
598+
}
599+
600+
It("detect when an instance is fenced", func() {
601+
Expect(cluster.IsInstanceFenced("one")).To(BeTrue())
602+
})
603+
604+
It("detect when an instance is not fenced", func() {
605+
Expect(cluster.IsInstanceFenced("two")).To(BeFalse())
606+
})
607+
})
608+
609+
When("the whole cluster is fenced", func() {
610+
cluster := Cluster{
611+
ObjectMeta: v1.ObjectMeta{
612+
Annotations: map[string]string{
613+
utils.FencedInstanceAnnotation: "[\"*\"]",
614+
},
615+
},
616+
}
617+
618+
It("detect when an instance is fenced", func() {
619+
Expect(cluster.IsInstanceFenced("one")).To(BeTrue())
620+
Expect(cluster.IsInstanceFenced("two")).To(BeTrue())
621+
Expect(cluster.IsInstanceFenced("three")).To(BeTrue())
622+
})
623+
})
624+
625+
When("the annotation doesn't exist", func() {
626+
cluster := Cluster{
627+
ObjectMeta: v1.ObjectMeta{
628+
Annotations: map[string]string{},
629+
},
630+
}
631+
632+
It("ensure no instances are fenced", func() {
633+
Expect(cluster.IsInstanceFenced("one")).To(BeFalse())
634+
})
635+
})
636+
})

cmd/kubectl-cnp/main.go

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
"github.com/EnterpriseDB/cloud-native-postgresql/internal/cmd/plugin"
1919
"github.com/EnterpriseDB/cloud-native-postgresql/internal/cmd/plugin/certificate"
20+
"github.com/EnterpriseDB/cloud-native-postgresql/internal/cmd/plugin/fence"
2021
"github.com/EnterpriseDB/cloud-native-postgresql/internal/cmd/plugin/promote"
2122
"github.com/EnterpriseDB/cloud-native-postgresql/internal/cmd/plugin/reload"
2223
"github.com/EnterpriseDB/cloud-native-postgresql/internal/cmd/plugin/report"
@@ -42,6 +43,7 @@ func main() {
4243
rootCmd.AddCommand(status.NewCmd())
4344
rootCmd.AddCommand(promote.NewCmd())
4445
rootCmd.AddCommand(certificate.NewCmd())
46+
rootCmd.AddCommand(fence.NewCmd())
4547
rootCmd.AddCommand(restart.NewCmd())
4648
rootCmd.AddCommand(reload.NewCmd())
4749
rootCmd.AddCommand(versions.NewCmd())

controllers/cluster_status.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -684,7 +684,10 @@ func (r *ClusterReconciler) extractInstancesStatus(
684684

685685
for idx := range filteredPods {
686686
instanceStatus := r.getReplicaStatusFromPodViaHTTP(ctx, filteredPods[idx])
687-
instanceStatus.IsReady = utils.IsPodReady(filteredPods[idx])
687+
688+
// Here we need to have pod marked as ready even when they are fenced. We want this
689+
// to avoid a fenced primary to cause a failover to a different Pod.
690+
instanceStatus.IsReady = instanceStatus.IsReady || utils.IsPodReady(filteredPods[idx])
688691
instanceStatus.Node = filteredPods[idx].Spec.NodeName
689692
instanceStatus.Pod = filteredPods[idx]
690693

docs/mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ nav:
4141
- openshift.md
4242
- failover.md
4343
- troubleshooting.md
44+
- fencing.md
4445
- e2e.md
4546
- container_images.md
4647
- operator_capability_levels.md

docs/src/failover.md

-1
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,3 @@ Failover may result in the service being impacted and/or data being lost:
7373
level. On the contrary, setting it to a high value, might remove the risk of
7474
data loss while leaving the cluster without an active primary for a longer time
7575
during the switchover.
76-

docs/src/fencing.md

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Fencing
2+
3+
Fencing in Cloud Native PostgreSQL is the ultimate process of protecting the
4+
data in one, more, or even all instances of a PostgreSQL cluster when they
5+
appear to be malfunctioning. When an instance is fenced, the PostgreSQL server
6+
process (`postmaster`) is guaranteed to be shut down, while the pod is kept running.
7+
This makes sure that, until the fence is lifted, data on the pod is not modified by
8+
PostgreSQL and that the file system can be investigated for debugging and
9+
troubleshooting purposes.
10+
11+
## How to fence instances
12+
13+
In Cloud Native PostgreSQL you can fence:
14+
15+
- a specific instance
16+
- a list of instances
17+
- an entire Postgres `Cluster`
18+
19+
Fencing is controlled through the content of the `k8s.enterprisedb.io/fencedInstances`
20+
annotation, which expects a JSON formatted list of instance names.
21+
If the annotation is set to `'["*"]'`, a singleton list with a wildcard, the
22+
whole cluster is fenced.
23+
If the annotation is set to an empty JSON list, the operator behaves as if the
24+
annotation was not set.
25+
26+
For example:
27+
28+
- `k8s.enterprisedb.io/fencedInstances: '["cluster-example-1"]'` will fence just
29+
the `cluster-example-1` instance
30+
31+
- `k8s.enterprisedb.io/fencedInstances: '["cluster-example-1","cluster-example-2"]'`
32+
will fence the `cluster-example-1` and `cluster-example-2` instances
33+
34+
- `k8s.enterprisedb.io/fencedInstances: '["*"]'` will fence every instance in
35+
the cluster.
36+
37+
The annotation can be manually set on the Kubernetes object, for example via
38+
the `kubectl annotate` command, or in a transparent way using the
39+
`kubectl cnp fencing on` subcommand:
40+
41+
```shell
42+
# to fence only one instance
43+
kubectl cnp fencing on cluster-example 1
44+
45+
# to fence all the instances in a Cluster
46+
kubectl cnp fencing on cluster-example "*"
47+
```
48+
49+
Here is an example of a `Cluster` with an instance that was previously fenced:
50+
51+
```yaml
52+
apiVersion: postgresql.k8s.enterprisedb.io/v1
53+
kind: Cluster
54+
metadata:
55+
annotations:
56+
k8s.enterprisedb.io/fencedInstances: '["cluster-example-1"]'
57+
[...]
58+
```
59+
60+
## How to lift fencing
61+
62+
Fencing can be lifted by clearing the annotation, or set it to a different value.
63+
64+
As for fencing, this can be done either manually with `kubectl annotate`, or
65+
using the `kubectl cnp fencing` subcommand as follows:
66+
67+
```shell
68+
# to lift the fencing only for one instance
69+
# N.B.: at the moment this won't work if the whole cluster was fenced previously,
70+
# in that case you will have to manually set the annotation as explained above
71+
kubectl cnp fencing off cluster-example 1
72+
73+
# to lift the fencing for all the instances in a Cluster
74+
kubectl cnp fencing off cluster-example "*"
75+
```
76+
77+
## How fencing works
78+
79+
Once an instance is set for fencing, the procedure to shut down the
80+
`postmaster` process is initiated. This consists of an initial smart shutdown
81+
with a timeout set to `.spec.stopDelay`, followed by a fast shutdown if
82+
required. Then:
83+
84+
- the Pod will be kept alive
85+
86+
- the Pod won't be marked as *Ready*
87+
88+
- all the changes that don't require the Postgres instance to be up will be
89+
reconciled, including:
90+
- configuration files
91+
- certificates and all the cryptographic material
92+
93+
- metrics will not be collected, except `cnp_collector_fencing_on` which will be
94+
set to 1
95+
96+
!!! Warning
97+
When at least one instance in a `Cluster` is fenced, failovers/switchovers for that
98+
`Cluster` will be blocked until the fence is lifted, as the status of the `Cluster`
99+
cannot be considered stable.
100+
101+
In particular, if a **primary instance** will be fenced, the postmaster process
102+
will be shut down but no failover will happen, interrupting the operativity of
103+
the applications. When the fence will be lifted, the primary instance will be
104+
started up again without any failover happening.
105+
106+
Given that, we advise the user to fence only replica instances when possible.
107+
108+
!!! Warning
109+
If the primary is the only fenced instance in a `Cluster` and the pod is deleted, a
110+
failover will be performed. When the fence on the old primary is lifted, that instance
111+
is restarted as a standby (follower of the new primary).
112+
113+
If a fenced instance is deleted, the pod will be recreated normally, but the
114+
postmaster won't be started. This can be extremely helpful when instances
115+
are `Crashlooping`.

docs/src/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ architectures on OpenShift only.
100100
* Standard output logging of PostgreSQL error messages in JSON format
101101
* Support for the `restricted` security context constraint (SCC) in Red Hat OpenShift
102102
* `cnp` plugin for `kubectl`
103+
* Fencing of an entire PostgreSQL cluster, or a subset of the instances
103104
* Multi-arch format container images
104105

105106
## About this guide

docs/src/operator_capability_levels.md

+10
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,16 @@ in the maintenance window section enables to specify the strategy to be used:
399399
allocate new storage in a different PVC for the evicted instance or wait
400400
for the underlying node to be available again.
401401

402+
### Fencing
403+
404+
Fencing is the process of protecting the data in one, more, or even all
405+
instances of a PostgreSQL cluster when they appear to be malfunctioning.
406+
When an instance is fenced, the PostgreSQL server process is
407+
guaranteed to be shut down, while the pod is kept running. This makes sure
408+
that, until the fence is lifted, data on the pod is not modified by PostgreSQL
409+
and that the file system can be investigated for debugging and troubleshooting
410+
purposes.
411+
402412
### Reuse of Persistent Volumes storage in Pods
403413

404414
When the operator needs to create a pod that has been deleted by the user or

0 commit comments

Comments
 (0)