feature toggle for using maintenance windows (#3074)

* feature toggle for using maintenance windows
This commit is contained in:
Felix Kunde 2026-04-16 17:13:18 +02:00 committed by GitHub
parent e9478894a8
commit 39cc09ccaa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 73 additions and 16 deletions

View File

@ -79,6 +79,9 @@ spec:
enable_lazy_spilo_upgrade: enable_lazy_spilo_upgrade:
type: boolean type: boolean
default: false default: false
enable_maintenance_windows:
type: boolean
default: true
enable_pgversion_env_var: enable_pgversion_env_var:
type: boolean type: boolean
default: true default: true

View File

@ -27,6 +27,8 @@ configGeneral:
- "all" - "all"
# update only the statefulsets without immediately doing the rolling update # update only the statefulsets without immediately doing the rolling update
enable_lazy_spilo_upgrade: false enable_lazy_spilo_upgrade: false
# toogle to use maintenance windows feature
enable_maintenance_windows: true
# set the PGVERSION env var instead of providing the version via postgresql.bin_dir in SPILO_CONFIGURATION # set the PGVERSION env var instead of providing the version via postgresql.bin_dir in SPILO_CONFIGURATION
enable_pgversion_env_var: true enable_pgversion_env_var: true
# start any new database pod without limitations on shm memory # start any new database pod without limitations on shm memory

View File

@ -95,11 +95,11 @@ Thus, the `full` mode can create drift between desired and actual state.
### Upgrade during maintenance windows ### Upgrade during maintenance windows
When `maintenanceWindows` are defined in the Postgres manifest the operator When `maintenanceWindows` are defined in the Postgres manifest or in the global
will trigger major-version-related pod rotation and the major version upgrade config the operator will trigger major-version-related pod rotation and the
only during these periods. Make sure they are at least twice as long as your major version upgrade only during these periods. Make sure they are at least
configured `resync_period` to guarantee twice as long as your configured `resync_period` to guarantee that operator
that operator actions can be triggered. actions can be triggered.
### Upgrade annotations ### Upgrade annotations

View File

@ -118,7 +118,9 @@ These parameters are grouped directly under the `spec` key in the manifest.
a list which defines specific time frames when certain maintenance operations a list which defines specific time frames when certain maintenance operations
such as automatic major upgrades or master pod migration are allowed to happen. such as automatic major upgrades or master pod migration are allowed to happen.
Accepted formats are "01:00-06:00" for daily maintenance windows or Accepted formats are "01:00-06:00" for daily maintenance windows or
"Sat:00:00-04:00" for specific days, with all times in UTC. "Sat:00:00-04:00" for specific days, with all times in UTC. Note, when the
global config option `enable_maintenance_windows` is false, the specified
windows will be ignored.
* **users** * **users**
a map of usernames to user flags for the users that should be created in the a map of usernames to user flags for the users that should be created in the

View File

@ -173,6 +173,9 @@ Those are top-level keys, containing both leaf keys and groups.
the thresholds. The value must be `"true"` to be effective. The default is empty the thresholds. The value must be `"true"` to be effective. The default is empty
which means the feature is disabled. which means the feature is disabled.
* **enable_maintenance_windows**
toggle for using the maintenance windows feature. Default is `"true"`.
* **maintenance_windows** * **maintenance_windows**
a list which defines specific time frames when certain maintenance a list which defines specific time frames when certain maintenance
operations such as automatic major upgrades or master pod migration are operations such as automatic major upgrades or master pod migration are

View File

@ -46,6 +46,7 @@ data:
enable_ebs_gp3_migration_max_size: "1000" enable_ebs_gp3_migration_max_size: "1000"
enable_init_containers: "true" enable_init_containers: "true"
enable_lazy_spilo_upgrade: "false" enable_lazy_spilo_upgrade: "false"
enable_maintenance_windows: "true"
enable_master_load_balancer: "false" enable_master_load_balancer: "false"
enable_master_pooler_load_balancer: "false" enable_master_pooler_load_balancer: "false"
enable_password_rotation: "false" enable_password_rotation: "false"

View File

@ -77,6 +77,9 @@ spec:
enable_lazy_spilo_upgrade: enable_lazy_spilo_upgrade:
type: boolean type: boolean
default: false default: false
enable_maintenance_windows:
type: boolean
default: true
enable_pgversion_env_var: enable_pgversion_env_var:
type: boolean type: boolean
default: true default: true

View File

@ -8,6 +8,7 @@ configuration:
# crd_categories: # crd_categories:
# - all # - all
# enable_lazy_spilo_upgrade: false # enable_lazy_spilo_upgrade: false
enable_maintenance_windows: true
enable_pgversion_env_var: true enable_pgversion_env_var: true
# enable_shm_volume: true # enable_shm_volume: true
enable_spilo_wal_path_compat: false enable_spilo_wal_path_compat: false

View File

@ -105,6 +105,9 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{
"enable_lazy_spilo_upgrade": { "enable_lazy_spilo_upgrade": {
Type: "boolean", Type: "boolean",
}, },
"enable_maintenance_windows": {
Type: "boolean",
},
"enable_shm_volume": { "enable_shm_volume": {
Type: "boolean", Type: "boolean",
}, },

View File

@ -266,6 +266,7 @@ type OperatorConfigurationData struct {
Workers uint32 `json:"workers,omitempty"` Workers uint32 `json:"workers,omitempty"`
ResyncPeriod Duration `json:"resync_period,omitempty"` ResyncPeriod Duration `json:"resync_period,omitempty"`
RepairPeriod Duration `json:"repair_period,omitempty"` RepairPeriod Duration `json:"repair_period,omitempty"`
EnableMaintenanceWindows *bool `json:"enable_maintenance_windows,omitempty"`
MaintenanceWindows []MaintenanceWindow `json:"maintenance_windows,omitempty"` MaintenanceWindows []MaintenanceWindow `json:"maintenance_windows,omitempty"`
SetMemoryRequestToLimit bool `json:"set_memory_request_to_limit,omitempty"` SetMemoryRequestToLimit bool `json:"set_memory_request_to_limit,omitempty"`
ShmVolume *bool `json:"enable_shm_volume,omitempty"` ShmVolume *bool `json:"enable_shm_volume,omitempty"`

View File

@ -433,6 +433,11 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData
*out = make([]string, len(*in)) *out = make([]string, len(*in))
copy(*out, *in) copy(*out, *in)
} }
if in.EnableMaintenanceWindows != nil {
in, out := &in.EnableMaintenanceWindows, &out.EnableMaintenanceWindows
*out = new(bool)
**out = **in
}
if in.MaintenanceWindows != nil { if in.MaintenanceWindows != nil {
in, out := &in.MaintenanceWindows, &out.MaintenanceWindows in, out := &in.MaintenanceWindows, &out.MaintenanceWindows
*out = make([]MaintenanceWindow, len(*in)) *out = make([]MaintenanceWindow, len(*in))

View File

@ -675,7 +675,9 @@ func isStandbyCluster(spec *acidv1.PostgresSpec) bool {
} }
func (c *Cluster) isInMaintenanceWindow(specMaintenanceWindows []acidv1.MaintenanceWindow) bool { func (c *Cluster) isInMaintenanceWindow(specMaintenanceWindows []acidv1.MaintenanceWindow) bool {
if len(specMaintenanceWindows) == 0 && len(c.OpConfig.MaintenanceWindows) == 0 { ignoreMaintenanceWindows := c.OpConfig.EnableMaintenanceWindows != nil && !*c.OpConfig.EnableMaintenanceWindows
noWindowsDefined := len(specMaintenanceWindows) == 0 && len(c.OpConfig.MaintenanceWindows) == 0
if noWindowsDefined || ignoreMaintenanceWindows {
return true return true
} }
now := time.Now() now := time.Now()

View File

@ -660,6 +660,7 @@ func TestIsInMaintenanceWindow(t *testing.T) {
cluster := New( cluster := New(
Config{ Config{
OpConfig: config.Config{ OpConfig: config.Config{
EnableMaintenanceWindows: util.True(),
Resources: config.Resources{ Resources: config.Resources{
ClusterLabels: map[string]string{"application": "spilo"}, ClusterLabels: map[string]string{"application": "spilo"},
ClusterNameLabel: "cluster-name", ClusterNameLabel: "cluster-name",
@ -683,12 +684,27 @@ func TestIsInMaintenanceWindow(t *testing.T) {
name string name string
windows []acidv1.MaintenanceWindow windows []acidv1.MaintenanceWindow
configWindows []string configWindows []string
windowsFlag bool
expected bool expected bool
}{ }{
{ {
name: "no maintenance windows", name: "no maintenance windows",
windows: nil, windows: nil,
configWindows: nil, configWindows: nil,
windowsFlag: true,
expected: true,
},
{
name: "maintenance windows diabled",
windows: []acidv1.MaintenanceWindow{
{
Everyday: true,
StartTime: mustParseTime("00:00"),
EndTime: mustParseTime("23:59"),
},
},
configWindows: nil,
windowsFlag: false,
expected: true, expected: true,
}, },
{ {
@ -701,6 +717,7 @@ func TestIsInMaintenanceWindow(t *testing.T) {
}, },
}, },
configWindows: nil, configWindows: nil,
windowsFlag: true,
expected: true, expected: true,
}, },
{ {
@ -713,6 +730,7 @@ func TestIsInMaintenanceWindow(t *testing.T) {
}, },
}, },
configWindows: nil, configWindows: nil,
windowsFlag: true,
expected: true, expected: true,
}, },
{ {
@ -724,24 +742,35 @@ func TestIsInMaintenanceWindow(t *testing.T) {
EndTime: mustParseTime(futureTimeEndFormatted), EndTime: mustParseTime(futureTimeEndFormatted),
}, },
}, },
expected: false, windowsFlag: true,
expected: false,
}, },
{ {
name: "global maintenance windows with future interval time", name: "global maintenance windows with future interval time",
windows: nil, windows: nil,
configWindows: []string{fmt.Sprintf("%s-%s", futureTimeStartFormatted, futureTimeEndFormatted)}, configWindows: []string{fmt.Sprintf("%s-%s", futureTimeStartFormatted, futureTimeEndFormatted)},
windowsFlag: true,
expected: false, expected: false,
}, },
{ {
name: "global maintenance windows all day", name: "global maintenance windows all day",
windows: nil, windows: nil,
configWindows: []string{"00:00-02:00", "02:00-23:59"}, configWindows: []string{"00:00-02:00", "02:00-23:59"},
windowsFlag: true,
expected: true,
},
{
name: "global maintenance windows ignored",
windows: nil,
configWindows: []string{"00:00-02:00", "02:00-23:59"},
windowsFlag: false,
expected: true, expected: true,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
cluster.OpConfig.EnableMaintenanceWindows = &tt.windowsFlag
cluster.OpConfig.MaintenanceWindows = tt.configWindows cluster.OpConfig.MaintenanceWindows = tt.configWindows
cluster.Spec.MaintenanceWindows = tt.windows cluster.Spec.MaintenanceWindows = tt.windows
if cluster.isInMaintenanceWindow(cluster.Spec.MaintenanceWindows) != tt.expected { if cluster.isInMaintenanceWindow(cluster.Spec.MaintenanceWindows) != tt.expected {

View File

@ -51,6 +51,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.ShmVolume = util.CoalesceBool(fromCRD.ShmVolume, util.True()) result.ShmVolume = util.CoalesceBool(fromCRD.ShmVolume, util.True())
result.SidecarImages = fromCRD.SidecarImages result.SidecarImages = fromCRD.SidecarImages
result.SidecarContainers = fromCRD.SidecarContainers result.SidecarContainers = fromCRD.SidecarContainers
result.EnableMaintenanceWindows = util.CoalesceBool(fromCRD.EnableMaintenanceWindows, util.True())
if len(fromCRD.MaintenanceWindows) > 0 { if len(fromCRD.MaintenanceWindows) > 0 {
result.MaintenanceWindows = make([]string, 0, len(fromCRD.MaintenanceWindows)) result.MaintenanceWindows = make([]string, 0, len(fromCRD.MaintenanceWindows))
for _, window := range fromCRD.MaintenanceWindows { for _, window := range fromCRD.MaintenanceWindows {

View File

@ -173,14 +173,15 @@ type Config struct {
LogicalBackup LogicalBackup
ConnectionPooler ConnectionPooler
WatchedNamespace string `name:"watched_namespace"` // special values: "*" means 'watch all namespaces', the empty string "" means 'watch a namespace where operator is deployed to' WatchedNamespace string `name:"watched_namespace"` // special values: "*" means 'watch all namespaces', the empty string "" means 'watch a namespace where operator is deployed to'
KubernetesUseConfigMaps bool `name:"kubernetes_use_configmaps" default:"false"` KubernetesUseConfigMaps bool `name:"kubernetes_use_configmaps" default:"false"`
EtcdHost string `name:"etcd_host" default:""` // special values: the empty string "" means Patroni will use K8s as a DCS EtcdHost string `name:"etcd_host" default:""` // special values: the empty string "" means Patroni will use K8s as a DCS
MaintenanceWindows []string `name:"maintenance_windows"` EnableMaintenanceWindows *bool `name:"enable_maintenance_windows" default:"true"`
DockerImage string `name:"docker_image" default:"ghcr.io/zalando/spilo-18:4.1-p1"` MaintenanceWindows []string `name:"maintenance_windows"`
SidecarImages map[string]string `name:"sidecar_docker_images"` // deprecated in favour of SidecarContainers DockerImage string `name:"docker_image" default:"ghcr.io/zalando/spilo-18:4.1-p1"`
SidecarContainers []v1.Container `name:"sidecars"` SidecarImages map[string]string `name:"sidecar_docker_images"` // deprecated in favour of SidecarContainers
PodServiceAccountName string `name:"pod_service_account_name" default:"postgres-pod"` SidecarContainers []v1.Container `name:"sidecars"`
PodServiceAccountName string `name:"pod_service_account_name" default:"postgres-pod"`
// value of this string must be valid JSON or YAML; see initPodServiceAccount // value of this string must be valid JSON or YAML; see initPodServiceAccount
PodServiceAccountDefinition string `name:"pod_service_account_definition" default:""` PodServiceAccountDefinition string `name:"pod_service_account_definition" default:""`
PodServiceAccountRoleBindingDefinition string `name:"pod_service_account_role_binding_definition" default:""` PodServiceAccountRoleBindingDefinition string `name:"pod_service_account_role_binding_definition" default:""`