add support for global maintenance windows (#3038)

* add support for global maintenance windows
* fix schema validation and trim \ when unmarshalling maintenance window
This commit is contained in:
Felix Kunde
2026-01-30 11:37:21 +01:00
committed by GitHub
parent 137f3e769d
commit b84c58c2a6
19 changed files with 131 additions and 29 deletions
+1 -1
View File
@@ -989,7 +989,7 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
return fmt.Errorf("could not set cluster status to updating: %w", err)
}
if !isInMaintenanceWindow(newSpec.Spec.MaintenanceWindows) {
if !c.isInMaintenanceWindow(newSpec.Spec.MaintenanceWindows) {
// do not apply any major version related changes yet
newSpec.Spec.PostgresqlParam.PgVersion = oldSpec.Spec.PostgresqlParam.PgVersion
}
+1 -1
View File
@@ -197,7 +197,7 @@ func (c *Cluster) majorVersionUpgrade() error {
return nil
}
if !isInMaintenanceWindow(c.Spec.MaintenanceWindows) {
if !c.isInMaintenanceWindow(c.Spec.MaintenanceWindows) {
c.logger.Infof("skipping major version upgrade, not in maintenance window")
return nil
}
+1 -1
View File
@@ -280,7 +280,7 @@ func (c *Cluster) MigrateMasterPod(podName spec.NamespacedName) error {
}
scheduleSwitchover := false
if !isInMaintenanceWindow(c.Spec.MaintenanceWindows) {
if !c.isInMaintenanceWindow(c.Spec.MaintenanceWindows) {
c.logger.Infof("postponing switchover, not in maintenance window")
scheduleSwitchover = true
}
+1 -1
View File
@@ -97,7 +97,7 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error {
}
}
if !isInMaintenanceWindow(newSpec.Spec.MaintenanceWindows) {
if !c.isInMaintenanceWindow(newSpec.Spec.MaintenanceWindows) {
// do not apply any major version related changes yet
newSpec.Spec.PostgresqlParam.PgVersion = oldSpec.Spec.PostgresqlParam.PgVersion
}
+16 -3
View File
@@ -663,15 +663,28 @@ func parseResourceRequirements(resourcesRequirement v1.ResourceRequirements) (ac
return resources, nil
}
func isInMaintenanceWindow(specMaintenanceWindows []acidv1.MaintenanceWindow) bool {
if len(specMaintenanceWindows) == 0 {
func (c *Cluster) isInMaintenanceWindow(specMaintenanceWindows []acidv1.MaintenanceWindow) bool {
if len(specMaintenanceWindows) == 0 && len(c.OpConfig.MaintenanceWindows) == 0 {
return true
}
now := time.Now()
currentDay := now.Weekday()
currentTime := now.Format("15:04")
for _, window := range specMaintenanceWindows {
maintenanceWindows := specMaintenanceWindows
if len(maintenanceWindows) == 0 {
maintenanceWindows = make([]acidv1.MaintenanceWindow, 0, len(c.OpConfig.MaintenanceWindows))
for _, windowStr := range c.OpConfig.MaintenanceWindows {
var window acidv1.MaintenanceWindow
if err := window.UnmarshalJSON([]byte(windowStr)); err != nil {
c.logger.Errorf("could not parse default maintenance window %q: %v", windowStr, err)
continue
}
maintenanceWindows = append(maintenanceWindows, window)
}
}
for _, window := range maintenanceWindows {
startTime := window.StartTime.Format("15:04")
endTime := window.EndTime.Format("15:04")
+42 -9
View File
@@ -657,6 +657,22 @@ func Test_trimCronjobName(t *testing.T) {
}
func TestIsInMaintenanceWindow(t *testing.T) {
cluster := New(
Config{
OpConfig: config.Config{
Resources: config.Resources{
ClusterLabels: map[string]string{"application": "spilo"},
ClusterNameLabel: "cluster-name",
DefaultCPURequest: "300m",
DefaultCPULimit: "300m",
DefaultMemoryRequest: "300Mi",
DefaultMemoryLimit: "300Mi",
},
},
}, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder)
cluster.Name = clusterName
cluster.Namespace = namespace
now := time.Now()
futureTimeStart := now.Add(1 * time.Hour)
futureTimeStartFormatted := futureTimeStart.Format("15:04")
@@ -664,14 +680,16 @@ func TestIsInMaintenanceWindow(t *testing.T) {
futureTimeEndFormatted := futureTimeEnd.Format("15:04")
tests := []struct {
name string
windows []acidv1.MaintenanceWindow
expected bool
name string
windows []acidv1.MaintenanceWindow
configWindows []string
expected bool
}{
{
name: "no maintenance windows",
windows: nil,
expected: true,
name: "no maintenance windows",
windows: nil,
configWindows: nil,
expected: true,
},
{
name: "maintenance windows with everyday",
@@ -682,7 +700,8 @@ func TestIsInMaintenanceWindow(t *testing.T) {
EndTime: mustParseTime("23:59"),
},
},
expected: true,
configWindows: nil,
expected: true,
},
{
name: "maintenance windows with weekday",
@@ -693,7 +712,8 @@ func TestIsInMaintenanceWindow(t *testing.T) {
EndTime: mustParseTime("23:59"),
},
},
expected: true,
configWindows: nil,
expected: true,
},
{
name: "maintenance windows with future interval time",
@@ -706,12 +726,25 @@ func TestIsInMaintenanceWindow(t *testing.T) {
},
expected: false,
},
{
name: "global maintenance windows with future interval time",
windows: nil,
configWindows: []string{fmt.Sprintf("%s-%s", futureTimeStartFormatted, futureTimeEndFormatted)},
expected: false,
},
{
name: "global maintenance windows all day",
windows: nil,
configWindows: []string{"00:00-23:59"},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cluster.OpConfig.MaintenanceWindows = tt.configWindows
cluster.Spec.MaintenanceWindows = tt.windows
if isInMaintenanceWindow(cluster.Spec.MaintenanceWindows) != tt.expected {
if cluster.isInMaintenanceWindow(cluster.Spec.MaintenanceWindows) != tt.expected {
t.Errorf("Expected isInMaintenanceWindow to return %t", tt.expected)
}
})