mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 01:28:45 +01:00
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>
This commit is contained in:
5
docs/operator-manual/resource_actions_builtin.md
generated
5
docs/operator-manual/resource_actions_builtin.md
generated
@@ -64,6 +64,11 @@
|
||||
- [numaplane.numaproj.io/PipelineRollout/pause](https://github.com/argoproj/argo-cd/blob/master/resource_customizations/numaplane.numaproj.io/PipelineRollout/actions/pause/action.lua)
|
||||
- [numaplane.numaproj.io/PipelineRollout/unpause-fast](https://github.com/argoproj/argo-cd/blob/master/resource_customizations/numaplane.numaproj.io/PipelineRollout/actions/unpause-fast/action.lua)
|
||||
- [numaplane.numaproj.io/PipelineRollout/unpause-gradual](https://github.com/argoproj/argo-cd/blob/master/resource_customizations/numaplane.numaproj.io/PipelineRollout/actions/unpause-gradual/action.lua)
|
||||
- [postgresql.cnpg.io/Cluster/promote](https://github.com/argoproj/argo-cd/blob/master/resource_customizations/postgresql.cnpg.io/Cluster/actions/promote/action.lua)
|
||||
- [postgresql.cnpg.io/Cluster/reload](https://github.com/argoproj/argo-cd/blob/master/resource_customizations/postgresql.cnpg.io/Cluster/actions/reload/action.lua)
|
||||
- [postgresql.cnpg.io/Cluster/restart](https://github.com/argoproj/argo-cd/blob/master/resource_customizations/postgresql.cnpg.io/Cluster/actions/restart/action.lua)
|
||||
- [postgresql.cnpg.io/Cluster/resume](https://github.com/argoproj/argo-cd/blob/master/resource_customizations/postgresql.cnpg.io/Cluster/actions/resume/action.lua)
|
||||
- [postgresql.cnpg.io/Cluster/suspend](https://github.com/argoproj/argo-cd/blob/master/resource_customizations/postgresql.cnpg.io/Cluster/actions/suspend/action.lua)
|
||||
- [promoter.argoproj.io/PullRequest/merge](https://github.com/argoproj/argo-cd/blob/master/resource_customizations/promoter.argoproj.io/PullRequest/actions/merge/action.lua)
|
||||
- [source.toolkit.fluxcd.io/Bucket/reconcile](https://github.com/argoproj/argo-cd/blob/master/resource_customizations/source.toolkit.fluxcd.io/Bucket/actions/reconcile/action.lua)
|
||||
- [source.toolkit.fluxcd.io/Bucket/resume](https://github.com/argoproj/argo-cd/blob/master/resource_customizations/source.toolkit.fluxcd.io/Bucket/actions/resume/action.lua)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,50 @@
|
||||
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]
|
||||
elseif #healthy > 0 then
|
||||
selected = healthy[1] -- fallback to first healthy if current primary not healthy
|
||||
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
|
||||
@@ -0,0 +1,10 @@
|
||||
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
|
||||
@@ -0,0 +1,10 @@
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
23
resource_customizations/postgresql.cnpg.io/Cluster/actions/testdata/cluster_healthy.yaml
vendored
Normal file
23
resource_customizations/postgresql.cnpg.io/Cluster/actions/testdata/cluster_healthy.yaml
vendored
Normal file
@@ -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"
|
||||
24
resource_customizations/postgresql.cnpg.io/Cluster/actions/testdata/cluster_promoting.yaml
vendored
Normal file
24
resource_customizations/postgresql.cnpg.io/Cluster/actions/testdata/cluster_promoting.yaml
vendored
Normal file
@@ -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"
|
||||
@@ -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"
|
||||
25
resource_customizations/postgresql.cnpg.io/Cluster/actions/testdata/cluster_reload.yaml
vendored
Normal file
25
resource_customizations/postgresql.cnpg.io/Cluster/actions/testdata/cluster_reload.yaml
vendored
Normal file
@@ -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"
|
||||
25
resource_customizations/postgresql.cnpg.io/Cluster/actions/testdata/cluster_restart.yaml
vendored
Normal file
25
resource_customizations/postgresql.cnpg.io/Cluster/actions/testdata/cluster_restart.yaml
vendored
Normal file
@@ -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"
|
||||
@@ -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",
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
178
resource_customizations/postgresql.cnpg.io/Cluster/testdata/cluster_reconcile_suspended.yaml
vendored
Normal file
178
resource_customizations/postgresql.cnpg.io/Cluster/testdata/cluster_reconcile_suspended.yaml
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Cluster
|
||||
metadata:
|
||||
annotations:
|
||||
cnpg.io/reconciliation: "disabled"
|
||||
kubectl.kubernetes.io/last-applied-configuration: |
|
||||
{"apiVersion":"postgresql.cnpg.io/v1","kind":"Cluster","metadata":{"annotations":{},"name":"cluster-example","namespace":"default"},"spec":{"imageName":"ghcr.io/cloudnative-pg/postgresql:13","instances":3,"storage":{"size":"1Gi"}}}
|
||||
creationTimestamp: "2025-04-25T20:44:24Z"
|
||||
generation: 1
|
||||
name: cluster-example
|
||||
namespace: default
|
||||
resourceVersion: "20230"
|
||||
uid: 987fe1ba-bba7-4021-9d25-f06ca9a8c0d2
|
||||
spec:
|
||||
affinity:
|
||||
podAntiAffinityType: preferred
|
||||
bootstrap:
|
||||
initdb:
|
||||
database: app
|
||||
encoding: UTF8
|
||||
localeCType: C
|
||||
localeCollate: C
|
||||
owner: app
|
||||
enablePDB: true
|
||||
enableSuperuserAccess: false
|
||||
failoverDelay: 0
|
||||
imageName: ghcr.io/cloudnative-pg/postgresql:13
|
||||
instances: 3
|
||||
logLevel: info
|
||||
maxSyncReplicas: 0
|
||||
minSyncReplicas: 0
|
||||
monitoring:
|
||||
customQueriesConfigMap:
|
||||
- key: queries
|
||||
name: cnpg-default-monitoring
|
||||
disableDefaultQueries: false
|
||||
enablePodMonitor: false
|
||||
postgresGID: 26
|
||||
postgresUID: 26
|
||||
postgresql:
|
||||
parameters:
|
||||
archive_mode: "on"
|
||||
archive_timeout: 5min
|
||||
dynamic_shared_memory_type: posix
|
||||
full_page_writes: "on"
|
||||
log_destination: csvlog
|
||||
log_directory: /controller/log
|
||||
log_filename: postgres
|
||||
log_rotation_age: "0"
|
||||
log_rotation_size: "0"
|
||||
log_truncate_on_rotation: "false"
|
||||
logging_collector: "on"
|
||||
max_parallel_workers: "32"
|
||||
max_replication_slots: "32"
|
||||
max_worker_processes: "32"
|
||||
shared_memory_type: mmap
|
||||
shared_preload_libraries: ""
|
||||
ssl_max_protocol_version: TLSv1.3
|
||||
ssl_min_protocol_version: TLSv1.3
|
||||
wal_keep_size: 512MB
|
||||
wal_level: logical
|
||||
wal_log_hints: "on"
|
||||
wal_receiver_timeout: 5s
|
||||
wal_sender_timeout: 5s
|
||||
syncReplicaElectionConstraint:
|
||||
enabled: false
|
||||
primaryUpdateMethod: restart
|
||||
primaryUpdateStrategy: unsupervised
|
||||
replicationSlots:
|
||||
highAvailability:
|
||||
enabled: true
|
||||
slotPrefix: _cnpg_
|
||||
synchronizeReplicas:
|
||||
enabled: true
|
||||
updateInterval: 30
|
||||
resources: {}
|
||||
smartShutdownTimeout: 180
|
||||
startDelay: 3600
|
||||
stopDelay: 1800
|
||||
storage:
|
||||
resizeInUseVolumes: true
|
||||
size: 1Gi
|
||||
switchoverDelay: 3600
|
||||
status:
|
||||
availableArchitectures:
|
||||
- goArch: amd64
|
||||
hash: d54839c128b2b38034c6f73006b2a979d916c9715cda5d59a1241018cc44904e
|
||||
certificates:
|
||||
clientCASecret: cluster-example-ca
|
||||
expirations:
|
||||
cluster-example-ca: 2025-07-24 20:39:24 +0000 UTC
|
||||
cluster-example-replication: 2025-07-24 20:39:25 +0000 UTC
|
||||
cluster-example-server: 2025-07-24 20:39:25 +0000 UTC
|
||||
replicationTLSSecret: cluster-example-replication
|
||||
serverAltDNSNames:
|
||||
- cluster-example-rw
|
||||
- cluster-example-rw.default
|
||||
- cluster-example-rw.default.svc
|
||||
- cluster-example-rw.default.svc.cluster.local
|
||||
- cluster-example-r
|
||||
- cluster-example-r.default
|
||||
- cluster-example-r.default.svc
|
||||
- cluster-example-r.default.svc.cluster.local
|
||||
- cluster-example-ro
|
||||
- cluster-example-ro.default
|
||||
- cluster-example-ro.default.svc
|
||||
- cluster-example-ro.default.svc.cluster.local
|
||||
serverCASecret: cluster-example-ca
|
||||
serverTLSSecret: cluster-example-server
|
||||
cloudNativePGCommitHash: 005e82a17
|
||||
cloudNativePGOperatorHash: d54839c128b2b38034c6f73006b2a979d916c9715cda5d59a1241018cc44904e
|
||||
conditions:
|
||||
- lastTransitionTime: "2025-04-25T20:45:33Z"
|
||||
message: Cluster is Ready
|
||||
reason: ClusterIsReady
|
||||
status: "True"
|
||||
type: Ready
|
||||
- lastTransitionTime: "2025-04-25T20:44:53Z"
|
||||
message: Continuous archiving is working
|
||||
reason: ContinuousArchivingSuccess
|
||||
status: "True"
|
||||
type: ContinuousArchiving
|
||||
configMapResourceVersion:
|
||||
metrics:
|
||||
cnpg-default-monitoring: "19786"
|
||||
currentPrimary: cluster-example-1
|
||||
currentPrimaryTimestamp: "2025-04-25T20:44:38.190232Z"
|
||||
healthyPVC:
|
||||
- cluster-example-1
|
||||
- cluster-example-2
|
||||
- cluster-example-3
|
||||
image: ghcr.io/cloudnative-pg/postgresql:13
|
||||
instanceNames:
|
||||
- cluster-example-1
|
||||
- cluster-example-2
|
||||
- cluster-example-3
|
||||
instances: 3
|
||||
instancesReportedState:
|
||||
cluster-example-1:
|
||||
isPrimary: true
|
||||
timeLineID: 1
|
||||
cluster-example-2:
|
||||
isPrimary: false
|
||||
timeLineID: 1
|
||||
cluster-example-3:
|
||||
isPrimary: false
|
||||
timeLineID: 1
|
||||
instancesStatus:
|
||||
healthy:
|
||||
- cluster-example-1
|
||||
- cluster-example-2
|
||||
- cluster-example-3
|
||||
latestGeneratedNode: 3
|
||||
managedRolesStatus: {}
|
||||
phase: Cluster in healthy state
|
||||
poolerIntegrations:
|
||||
pgBouncerIntegration: {}
|
||||
pvcCount: 3
|
||||
readService: cluster-example-r
|
||||
readyInstances: 3
|
||||
secretsResourceVersion:
|
||||
applicationSecretVersion: "19755"
|
||||
clientCaSecretVersion: "19752"
|
||||
replicationSecretVersion: "19754"
|
||||
serverCaSecretVersion: "19752"
|
||||
serverSecretVersion: "19753"
|
||||
switchReplicaClusterStatus: {}
|
||||
targetPrimary: cluster-example-1
|
||||
targetPrimaryTimestamp: "2025-04-25T20:44:26.214164Z"
|
||||
timelineID: 1
|
||||
topology:
|
||||
instances:
|
||||
cluster-example-1: {}
|
||||
cluster-example-2: {}
|
||||
cluster-example-3: {}
|
||||
nodesUsed: 3
|
||||
successfullyExtracted: true
|
||||
writeService: cluster-example-rw
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/diff"
|
||||
@@ -26,61 +27,115 @@ func (t testNormalizer) Normalize(un *unstructured.Unstructured) error {
|
||||
if un == nil {
|
||||
return nil
|
||||
}
|
||||
switch un.GetKind() {
|
||||
case "Job":
|
||||
// Disambiguate resources by apiVersion group to avoid collisions on Kind names
|
||||
gv, err := schema.ParseGroupVersion(un.GetAPIVersion())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse apiVersion for %s: %w", un.GetKind(), err)
|
||||
}
|
||||
group := gv.Group
|
||||
// First, group-specific, then kind-specific normalization
|
||||
switch group {
|
||||
case "batch":
|
||||
if un.GetKind() == "Job" {
|
||||
return t.normalizeJob(un)
|
||||
case "DaemonSet", "Deployment", "StatefulSet":
|
||||
err := unstructured.SetNestedStringMap(un.Object, map[string]string{"kubectl.kubernetes.io/restartedAt": "0001-01-01T00:00:00Z"}, "spec", "template", "metadata", "annotations")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
}
|
||||
case "apps":
|
||||
switch un.GetKind() {
|
||||
case "Deployment":
|
||||
err := unstructured.SetNestedField(un.Object, nil, "status")
|
||||
if err != nil {
|
||||
case "DaemonSet", "Deployment", "StatefulSet":
|
||||
if err := setRestartedAtAnnotationOnPodTemplate(un); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
err = unstructured.SetNestedField(un.Object, nil, "metadata", "creationTimestamp")
|
||||
if err != nil {
|
||||
}
|
||||
if un.GetKind() == "Deployment" {
|
||||
if err := unstructured.SetNestedField(un.Object, nil, "status"); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
err = unstructured.SetNestedField(un.Object, nil, "metadata", "generation")
|
||||
if err != nil {
|
||||
if err := unstructured.SetNestedField(un.Object, nil, "metadata", "creationTimestamp"); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
if err := unstructured.SetNestedField(un.Object, nil, "metadata", "generation"); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
}
|
||||
case "argoproj.io":
|
||||
switch un.GetKind() {
|
||||
case "Rollout":
|
||||
err := unstructured.SetNestedField(un.Object, nil, "spec", "restartAt")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
case "ExternalSecret", "PushSecret":
|
||||
err := unstructured.SetNestedStringMap(un.Object, map[string]string{"force-sync": "0001-01-01T00:00:00Z"}, "metadata", "annotations")
|
||||
if err != nil {
|
||||
if err := unstructured.SetNestedField(un.Object, nil, "spec", "restartAt"); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
case "Workflow":
|
||||
err := unstructured.SetNestedField(un.Object, nil, "metadata", "resourceVersion")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to normalize Rollout: %w", err)
|
||||
if err := unstructured.SetNestedField(un.Object, nil, "metadata", "resourceVersion"); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
err = unstructured.SetNestedField(un.Object, nil, "metadata", "uid")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to normalize Rollout: %w", err)
|
||||
if err := unstructured.SetNestedField(un.Object, nil, "metadata", "uid"); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
err = unstructured.SetNestedField(un.Object, nil, "metadata", "annotations", "workflows.argoproj.io/scheduled-time")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to normalize Rollout: %w", err)
|
||||
}
|
||||
case "HelmRelease", "ImageRepository", "ImageUpdateAutomation", "Kustomization", "Receiver", "Bucket", "GitRepository", "HelmChart", "HelmRepository", "OCIRepository":
|
||||
err := unstructured.SetNestedStringMap(un.Object, map[string]string{"reconcile.fluxcd.io/requestedAt": "By Argo CD at: 0001-01-01T00:00:00"}, "metadata", "annotations")
|
||||
if err != nil {
|
||||
if err := unstructured.SetNestedField(un.Object, nil, "metadata", "annotations", "workflows.argoproj.io/scheduled-time"); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
}
|
||||
case "external-secrets.io":
|
||||
switch un.GetKind() {
|
||||
case "ExternalSecret", "PushSecret":
|
||||
if err := unstructured.SetNestedStringMap(un.Object, map[string]string{"force-sync": "0001-01-01T00:00:00Z"}, "metadata", "annotations"); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
}
|
||||
case "postgresql.cnpg.io":
|
||||
if un.GetKind() == "Cluster" {
|
||||
if err := unstructured.SetNestedStringMap(un.Object, map[string]string{"cnpg.io/reloadedAt": "0001-01-01T00:00:00Z", "kubectl.kubernetes.io/restartedAt": "0001-01-01T00:00:00Z"}, "metadata", "annotations"); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
if err := unstructured.SetNestedField(un.Object, nil, "status", "targetPrimaryTimestamp"); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
}
|
||||
case "helm.toolkit.fluxcd.io":
|
||||
if un.GetKind() == "HelmRelease" {
|
||||
if err := setFluxRequestedAtAnnotation(un); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
}
|
||||
case "source.toolkit.fluxcd.io":
|
||||
switch un.GetKind() {
|
||||
case "Bucket", "GitRepository", "HelmChart", "HelmRepository", "OCIRepository":
|
||||
if err := setFluxRequestedAtAnnotation(un); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
}
|
||||
case "image.toolkit.fluxcd.io":
|
||||
switch un.GetKind() {
|
||||
case "ImageRepository", "ImageUpdateAutomation":
|
||||
if err := setFluxRequestedAtAnnotation(un); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
}
|
||||
case "kustomize.toolkit.fluxcd.io":
|
||||
if un.GetKind() == "Kustomization" {
|
||||
if err := setFluxRequestedAtAnnotation(un); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
}
|
||||
case "notification.toolkit.fluxcd.io":
|
||||
if un.GetKind() == "Receiver" {
|
||||
if err := setFluxRequestedAtAnnotation(un); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper: normalize restart annotation on pod template used by apps workloads
|
||||
func setRestartedAtAnnotationOnPodTemplate(un *unstructured.Unstructured) error {
|
||||
return unstructured.SetNestedStringMap(un.Object, map[string]string{"kubectl.kubernetes.io/restartedAt": "0001-01-01T00:00:00Z"}, "spec", "template", "metadata", "annotations")
|
||||
}
|
||||
|
||||
// Helper: normalize Flux requestedAt annotation across FluxCD kinds
|
||||
func setFluxRequestedAtAnnotation(un *unstructured.Unstructured) error {
|
||||
return unstructured.SetNestedStringMap(un.Object, map[string]string{"reconcile.fluxcd.io/requestedAt": "By Argo CD at: 0001-01-01T00:00:00"}, "metadata", "annotations")
|
||||
}
|
||||
|
||||
func (t testNormalizer) normalizeJob(un *unstructured.Unstructured) error {
|
||||
if conditions, exist, err := unstructured.NestedSlice(un.Object, "status", "conditions"); err != nil {
|
||||
return fmt.Errorf("failed to normalize %s: %w", un.GetKind(), err)
|
||||
|
||||
Reference in New Issue
Block a user