Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2ef96ea
Add cloudnativepg cluster actions
rouke-broersma Aug 15, 2025
b41ee6f
Add tests
rouke-broersma Aug 18, 2025
844bcad
Add actions to built_in_actions docs
rouke-broersma Aug 18, 2025
81006c4
Merge branch 'master' into cloudnativepg-actions
rouke-broersma Aug 18, 2025
d5c80a2
Add extra null check in case there is no instance status available
rouke-broersma Aug 18, 2025
14189d1
fix test files
rouke-broersma Aug 18, 2025
afe759c
String is not available
rouke-broersma Aug 18, 2025
9e525e7
fix test names
rouke-broersma Aug 18, 2025
9c46a1b
fix test data paths
rouke-broersma Aug 18, 2025
a378994
Merge remote-tracking branch 'upstream/master' into cloudnativepg-act…
rouke-broersma Aug 18, 2025
bb3052e
Remove referenced action
rouke-broersma Aug 18, 2025
8c46daa
Remove unneccesary fields from test fields
rouke-broersma Aug 18, 2025
219ffac
fix test data
rouke-broersma Aug 18, 2025
ef7dd6f
Add custom normalizations for Cluster
rouke-broersma Aug 19, 2025
6e8de97
remove toString
rouke-broersma Aug 19, 2025
f72203d
Merge remote-tracking branch 'upstream/master' into cloudnativepg-act…
rouke-broersma Sep 8, 2025
134c1e9
Add suspend/resume reconcile action and health status
rouke-broersma Sep 8, 2025
207b8ab
Add suspend and resume actions
rouke-broersma Sep 8, 2025
bd452e8
Use recycle icon for restart action
rouke-broersma Sep 8, 2025
ec75073
rename
rouke-broersma Sep 8, 2025
e225aa8
Validate api versions in resource actions normalization
rouke-broersma Sep 8, 2025
b29e848
reverse
rouke-broersma Sep 8, 2025
6bf161e
Merge remote-tracking branch 'upstream/master' into cloudnativepg-act…
rouke-broersma Sep 8, 2025
83fe3b1
Merge remote-tracking branch 'upstream/master' into cloudnativepg-act…
rouke-broersma Sep 9, 2025
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 docs/operator-manual/resource_actions_builtin.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
actionTests:
- action: promote
inputPath: testdata/cluster_healthy.yaml
expectedOutputPath: testdata/cluster_promoting.yaml
parameters:
instance: 'any'
- action: promote
inputPath: testdata/cluster_healthy.yaml
expectedOutputPath: testdata/cluster_promoting.yaml
parameters:
instance: '2'
- action: promote
inputPath: testdata/cluster_healthy.yaml
expectedOutputPath: testdata/cluster_promoting.yaml
parameters:
instance: 'cluster-example-2'
- action: promote
inputPath: testdata/cluster_healthy.yaml
expectedErrorMessage: 'Could not find a healthy instance matching the criteria: nonexistent-instance'
parameters:
instance: 'nonexistent-instance'
- action: reload
inputPath: testdata/cluster_healthy.yaml
expectedOutputPath: testdata/cluster_reload.yaml
- action: restart
inputPath: testdata/cluster_healthy.yaml
expectedOutputPath: testdata/cluster_restart.yaml
- action: suspend
inputPath: testdata/cluster_healthy.yaml
expectedOutputPath: testdata/cluster_reconcile_suspended.yaml
- action: resume
inputPath: testdata/cluster_reconcile_suspended.yaml
expectedOutputPath: testdata/cluster_healthy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
local actions = {}
actions["restart"] = {
["iconClass"] = "fa fa-fw fa-recycle",
["displayName"] = "Rollout restart Cluster"
}
actions["reload"] = {
["iconClass"] = "fa fa-fw fa-rotate-right",
["displayName"] = "Reload all Configuration"
}
actions["promote"] = {
["iconClass"] = "fa fa-fw fa-angles-up",
["displayName"] = "Promote Replica to Primary",
["disabled"] = (not obj.status.instancesStatus or not obj.status.instancesStatus.healthy or #obj.status.instancesStatus.healthy < 2),
["params"] = {
{
["name"] = "instance",
["default"] = "any"
}
}
}

-- Check if reconciliation is currently suspended
local isSuspended = false
if obj.metadata and obj.metadata.annotations and obj.metadata.annotations["cnpg.io/reconciliation"] == "disabled" then
isSuspended = true
end

-- Add suspend/resume actions based on current state
if isSuspended then
actions["resume"] = {
["iconClass"] = "fa fa-fw fa-play",
["displayName"] = "Resume Reconciliation"
}
else
actions["suspend"] = {
["iconClass"] = "fa fa-fw fa-pause",
["displayName"] = "Suspend Reconciliation"
}
end

return actions
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
local os = require("os")
local instance = actionParams["instance"]
local healthy = obj.status.instancesStatus.healthy
local selected = nil

if instance == "any" then
-- Select next healthy instance after currentPrimary
local nextIndex = 0
for index, node in ipairs(healthy) do
if node == obj.status.currentPrimary then
nextIndex = index + 1
if nextIndex > #healthy then
nextIndex = 1
end
break
end
end
if nextIndex > 0 then
selected = healthy[nextIndex]
end
elseif type(instance) == "string" and tonumber(instance) then
-- Select by instance number
local wanted = (obj.metadata and obj.metadata.name or "") .. "-" .. instance
for _, node in ipairs(healthy or {}) do
if node == wanted then
selected = node
break
end
end
elseif type(instance) == "string" then
-- Select by full name
for _, node in ipairs(healthy) do
if node == instance then
selected = node
break
end
end
end

if selected then
obj.status.targetPrimary = selected
obj.status.targetPrimaryTimestamp = os.date("!%Y-%m-%dT%XZ")
obj.status.phase = "Switchover in progress"
obj.status.phaseReason = "Switching over to " .. selected
else
error("Could not find a healthy instance matching the criteria: " .. instance, 0)
end
return obj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
local os = require("os")
if obj.metadata == nil then
obj.metadata = {}
end
if obj.metadata.annotations == nil then
obj.metadata.annotations = {}
end
obj.metadata.annotations["cnpg.io/reloadedAt"] = os.date("!%Y-%m-%dT%XZ")
return obj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
local os = require("os")
if obj.metadata == nil then
obj.metadata = {}
end
if obj.metadata.annotations == nil then
obj.metadata.annotations = {}
end
obj.metadata.annotations["kubectl.kubernetes.io/restartedAt"] = os.date("!%Y-%m-%dT%XZ")
return obj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
if obj.metadata == nil then
obj.metadata = {}
end

if obj.metadata.annotations == nil then
obj.metadata.annotations = {}
end

obj.metadata.annotations["cnpg.io/reconciliation"] = "disabled"
return obj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
if obj.metadata == nil then
obj.metadata = {}
end

if obj.metadata.annotations == nil then
obj.metadata.annotations = {}
end

obj.metadata.annotations["cnpg.io/reconciliation"] = nil
return obj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
creationTimestamp: "2025-04-25T20:44:24Z"
generation: 1
name: cluster-example
namespace: default
resourceVersion: "20230"
uid: 987fe1ba-bba7-4021-9d25-f06ca9a8c0d2
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:13
instances: 3
status:
currentPrimary: cluster-example-1
currentPrimaryTimestamp: "2025-04-25T20:44:38.190232Z"
instancesStatus:
healthy:
- cluster-example-1
- cluster-example-2
- cluster-example-3
phase: Cluster in healthy state
targetPrimary: cluster-example-1
targetPrimaryTimestamp: "2025-04-25T20:44:26.214164Z"
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
creationTimestamp: "2025-04-25T20:44:24Z"
generation: 1
name: cluster-example
namespace: default
resourceVersion: "20230"
uid: 987fe1ba-bba7-4021-9d25-f06ca9a8c0d2
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:13
instances: 3
status:
currentPrimary: cluster-example-1
currentPrimaryTimestamp: "2025-04-25T20:44:38.190232Z"
instancesStatus:
healthy:
- cluster-example-1
- cluster-example-2
- cluster-example-3
phase: Switchover in progress
phaseReason: Switching over to cluster-example-2
targetPrimary: cluster-example-2
targetPrimaryTimestamp: "0001-01-01T00:00:00Z"
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
creationTimestamp: "2025-04-25T20:44:24Z"
generation: 1
name: cluster-example
namespace: default
resourceVersion: "20230"
uid: 987fe1ba-bba7-4021-9d25-f06ca9a8c0d2
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:13
instances: 3
status:
currentPrimary: cluster-example-1
currentPrimaryTimestamp: "2025-04-25T20:44:38.190232Z"
instancesStatus:
healthy:
- cluster-example-1
- cluster-example-2
- cluster-example-3
phase: Cluster in healthy state
targetPrimary: cluster-example-1
targetPrimaryTimestamp: "2025-04-25T20:44:26.214164Z"
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
annotations:
cnpg.io/reloadedAt: "0001-01-01T00:00:00Z"
creationTimestamp: "2025-04-25T20:44:24Z"
generation: 1
name: cluster-example
namespace: default
resourceVersion: "20230"
uid: 987fe1ba-bba7-4021-9d25-f06ca9a8c0d2
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:13
instances: 3
status:
currentPrimary: cluster-example-1
currentPrimaryTimestamp: "2025-04-25T20:44:38.190232Z"
instancesStatus:
healthy:
- cluster-example-1
- cluster-example-2
- cluster-example-3
phase: Cluster in healthy state
targetPrimary: cluster-example-1
targetPrimaryTimestamp: "2025-04-25T20:44:26.214164Z"
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
annotations:
kubectl.kubernetes.io/restartedAt: "0001-01-01T00:00:00Z"
creationTimestamp: "2025-04-25T20:44:24Z"
generation: 1
name: cluster-example
namespace: default
resourceVersion: "20230"
uid: 987fe1ba-bba7-4021-9d25-f06ca9a8c0d2
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:13
instances: 3
status:
currentPrimary: cluster-example-1
currentPrimaryTimestamp: "2025-04-25T20:44:38.190232Z"
instancesStatus:
healthy:
- cluster-example-1
- cluster-example-2
- cluster-example-3
phase: Cluster in healthy state
targetPrimary: cluster-example-1
targetPrimaryTimestamp: "2025-04-25T20:44:26.214164Z"
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ local hs = {}
local cnpgStatus = {
["Cluster in healthy state"] = "Healthy",
["Setting up primary"] = "Progressing",
["Setting up primary"] = "Progressing",
["Creating a new replica"] = "Progressing",
["Upgrading cluster"] = "Progressing",
["Waiting for the instances to become active"] = "Progressing",
Expand Down Expand Up @@ -33,6 +32,13 @@ function hibernating(obj)
return nil
end

-- Check if reconciliation is suspended, since this is an explicit user action we return the "suspended" status immediately
if obj.metadata and obj.metadata.annotations and obj.metadata.annotations["cnpg.io/reconciliation"] == "disabled" then
hs.status = "Suspended"
hs.message = "Cluster reconciliation is suspended"
return hs
end

if obj.status ~= nil and obj.status.conditions ~= nil then
local hibernation = hibernating(obj)
if hibernation ~= nil then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ tests:
status: Degraded
message: "Initiating a failover from cluster-example-2"
inputPath: testdata/cluster_degraded.yaml
- healthStatus:
status: Suspended
message: "Cluster reconciliation is suspended"
inputPath: testdata/cluster_reconcile_suspended.yaml
Loading
Loading