Skip to content

Commit e50dd00

Browse files
feat(actions): Add cloudnativepg reload, restart, promote, suspend and resume actions (#24192)
Signed-off-by: Rouke Broersma <mobrockers@gmail.com> Signed-off-by: Rouke Broersma <rouke.broersma@infosupport.com>
1 parent 2c6edd8 commit e50dd00

File tree

17 files changed

+574
-42
lines changed

17 files changed

+574
-42
lines changed

docs/operator-manual/resource_actions_builtin.md

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
actionTests:
2+
- action: promote
3+
inputPath: testdata/cluster_healthy.yaml
4+
expectedOutputPath: testdata/cluster_promoting.yaml
5+
parameters:
6+
instance: 'any'
7+
- action: promote
8+
inputPath: testdata/cluster_healthy.yaml
9+
expectedOutputPath: testdata/cluster_promoting.yaml
10+
parameters:
11+
instance: '2'
12+
- action: promote
13+
inputPath: testdata/cluster_healthy.yaml
14+
expectedOutputPath: testdata/cluster_promoting.yaml
15+
parameters:
16+
instance: 'cluster-example-2'
17+
- action: promote
18+
inputPath: testdata/cluster_healthy.yaml
19+
expectedErrorMessage: 'Could not find a healthy instance matching the criteria: nonexistent-instance'
20+
parameters:
21+
instance: 'nonexistent-instance'
22+
- action: reload
23+
inputPath: testdata/cluster_healthy.yaml
24+
expectedOutputPath: testdata/cluster_reload.yaml
25+
- action: restart
26+
inputPath: testdata/cluster_healthy.yaml
27+
expectedOutputPath: testdata/cluster_restart.yaml
28+
- action: suspend
29+
inputPath: testdata/cluster_healthy.yaml
30+
expectedOutputPath: testdata/cluster_reconcile_suspended.yaml
31+
- action: resume
32+
inputPath: testdata/cluster_reconcile_suspended.yaml
33+
expectedOutputPath: testdata/cluster_healthy.yaml
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
local actions = {}
2+
actions["restart"] = {
3+
["iconClass"] = "fa fa-fw fa-recycle",
4+
["displayName"] = "Rollout restart Cluster"
5+
}
6+
actions["reload"] = {
7+
["iconClass"] = "fa fa-fw fa-rotate-right",
8+
["displayName"] = "Reload all Configuration"
9+
}
10+
actions["promote"] = {
11+
["iconClass"] = "fa fa-fw fa-angles-up",
12+
["displayName"] = "Promote Replica to Primary",
13+
["disabled"] = (not obj.status.instancesStatus or not obj.status.instancesStatus.healthy or #obj.status.instancesStatus.healthy < 2),
14+
["params"] = {
15+
{
16+
["name"] = "instance",
17+
["default"] = "any"
18+
}
19+
}
20+
}
21+
22+
-- Check if reconciliation is currently suspended
23+
local isSuspended = false
24+
if obj.metadata and obj.metadata.annotations and obj.metadata.annotations["cnpg.io/reconciliation"] == "disabled" then
25+
isSuspended = true
26+
end
27+
28+
-- Add suspend/resume actions based on current state
29+
if isSuspended then
30+
actions["resume"] = {
31+
["iconClass"] = "fa fa-fw fa-play",
32+
["displayName"] = "Resume Reconciliation"
33+
}
34+
else
35+
actions["suspend"] = {
36+
["iconClass"] = "fa fa-fw fa-pause",
37+
["displayName"] = "Suspend Reconciliation"
38+
}
39+
end
40+
41+
return actions
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
local os = require("os")
2+
local instance = actionParams["instance"]
3+
local healthy = obj.status.instancesStatus.healthy
4+
local selected = nil
5+
6+
if instance == "any" then
7+
-- Select next healthy instance after currentPrimary
8+
local nextIndex = 0
9+
for index, node in ipairs(healthy) do
10+
if node == obj.status.currentPrimary then
11+
nextIndex = index + 1
12+
if nextIndex > #healthy then
13+
nextIndex = 1
14+
end
15+
break
16+
end
17+
end
18+
if nextIndex > 0 then
19+
selected = healthy[nextIndex]
20+
elseif #healthy > 0 then
21+
selected = healthy[1] -- fallback to first healthy if current primary not healthy
22+
end
23+
elseif type(instance) == "string" and tonumber(instance) then
24+
-- Select by instance number
25+
local wanted = (obj.metadata and obj.metadata.name or "") .. "-" .. instance
26+
for _, node in ipairs(healthy or {}) do
27+
if node == wanted then
28+
selected = node
29+
break
30+
end
31+
end
32+
elseif type(instance) == "string" then
33+
-- Select by full name
34+
for _, node in ipairs(healthy) do
35+
if node == instance then
36+
selected = node
37+
break
38+
end
39+
end
40+
end
41+
42+
if selected then
43+
obj.status.targetPrimary = selected
44+
obj.status.targetPrimaryTimestamp = os.date("!%Y-%m-%dT%XZ")
45+
obj.status.phase = "Switchover in progress"
46+
obj.status.phaseReason = "Switching over to " .. selected
47+
else
48+
error("Could not find a healthy instance matching the criteria: " .. instance, 0)
49+
end
50+
return obj
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
local os = require("os")
2+
if obj.metadata == nil then
3+
obj.metadata = {}
4+
end
5+
if obj.metadata.annotations == nil then
6+
obj.metadata.annotations = {}
7+
end
8+
9+
obj.metadata.annotations["cnpg.io/reloadedAt"] = os.date("!%Y-%m-%dT%XZ")
10+
return obj
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
local os = require("os")
2+
if obj.metadata == nil then
3+
obj.metadata = {}
4+
end
5+
if obj.metadata.annotations == nil then
6+
obj.metadata.annotations = {}
7+
end
8+
9+
obj.metadata.annotations["kubectl.kubernetes.io/restartedAt"] = os.date("!%Y-%m-%dT%XZ")
10+
return obj
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
if obj.metadata == nil then
2+
obj.metadata = {}
3+
end
4+
5+
if obj.metadata.annotations == nil then
6+
obj.metadata.annotations = {}
7+
end
8+
9+
obj.metadata.annotations["cnpg.io/reconciliation"] = nil
10+
return obj
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
if obj.metadata == nil then
2+
obj.metadata = {}
3+
end
4+
5+
if obj.metadata.annotations == nil then
6+
obj.metadata.annotations = {}
7+
end
8+
9+
obj.metadata.annotations["cnpg.io/reconciliation"] = "disabled"
10+
return obj
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
apiVersion: postgresql.cnpg.io/v1
2+
kind: Cluster
3+
metadata:
4+
creationTimestamp: "2025-04-25T20:44:24Z"
5+
generation: 1
6+
name: cluster-example
7+
namespace: default
8+
resourceVersion: "20230"
9+
uid: 987fe1ba-bba7-4021-9d25-f06ca9a8c0d2
10+
spec:
11+
imageName: ghcr.io/cloudnative-pg/postgresql:13
12+
instances: 3
13+
status:
14+
currentPrimary: cluster-example-1
15+
currentPrimaryTimestamp: "2025-04-25T20:44:38.190232Z"
16+
instancesStatus:
17+
healthy:
18+
- cluster-example-1
19+
- cluster-example-2
20+
- cluster-example-3
21+
phase: Cluster in healthy state
22+
targetPrimary: cluster-example-1
23+
targetPrimaryTimestamp: "2025-04-25T20:44:26.214164Z"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
apiVersion: postgresql.cnpg.io/v1
2+
kind: Cluster
3+
metadata:
4+
creationTimestamp: "2025-04-25T20:44:24Z"
5+
generation: 1
6+
name: cluster-example
7+
namespace: default
8+
resourceVersion: "20230"
9+
uid: 987fe1ba-bba7-4021-9d25-f06ca9a8c0d2
10+
spec:
11+
imageName: ghcr.io/cloudnative-pg/postgresql:13
12+
instances: 3
13+
status:
14+
currentPrimary: cluster-example-1
15+
currentPrimaryTimestamp: "2025-04-25T20:44:38.190232Z"
16+
instancesStatus:
17+
healthy:
18+
- cluster-example-1
19+
- cluster-example-2
20+
- cluster-example-3
21+
phase: Switchover in progress
22+
phaseReason: Switching over to cluster-example-2
23+
targetPrimary: cluster-example-2
24+
targetPrimaryTimestamp: "0001-01-01T00:00:00Z"

0 commit comments

Comments
 (0)