Skip to content

Commit c31c99d

Browse files
authored
CLOUDP-126252: Maintenance Windows support (#563)
* WIP Maintenance Window Support * Maintenance Window CRD * Update sample to include maintenance window * Handling window deletion * Delete unused method * Improve documentation * Remove TODOs, set condition and event in controller * Defer and AutoDefer fields * Support of all API calls * Improve error text * Update maintenancewindow.go * Unit tests for window validation * Update sample * New reconciliation logic, one less field for window * Create matcher for window IT tests * make generate and manifests * Input validation * Manual conversion to Atlas * Update maintenancewindow.go * Integration tests * Fix pointers in ToAtlas * One more test * Remove TODO * Fix error log * Fix tests * Implementing pull request feedback * Implementing 2nd feedback
1 parent 06f9878 commit c31c99d

14 files changed

+548
-3
lines changed

config/crd/bases/atlas.mongodb.com_atlasdeployments.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ spec:
1919
- name: v1
2020
schema:
2121
openAPIV3Schema:
22-
description: AtlasDeployment is the Schema for the atlasclusters API
22+
description: AtlasDeployment is the Schema for the atlasdeployments API
2323
properties:
2424
apiVersion:
2525
description: 'APIVersion defines the versioned schema of this representation

config/crd/bases/atlas.mongodb.com_atlasprojects.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,40 @@ spec:
227227
type: object
228228
type: object
229229
type: array
230+
maintenanceWindow:
231+
description: MaintenanceWindow allows to specify a preferred time
232+
in the week to run maintenance operations. See more information
233+
at https://www.mongodb.com/docs/atlas/reference/api/maintenance-windows/
234+
properties:
235+
autoDefer:
236+
description: Flag indicating whether any scheduled project maintenance
237+
should be deferred automatically for one week.
238+
type: boolean
239+
dayOfWeek:
240+
description: Day of the week when you would like the maintenance
241+
window to start as a 1-based integer. Sunday 1, Monday 2, Tuesday
242+
3, Wednesday 4, Thursday 5, Friday 6, Saturday 7
243+
maximum: 7
244+
minimum: 1
245+
type: integer
246+
defer:
247+
description: Flag indicating whether the next scheduled project
248+
maintenance should be deferred for one week. Cannot be specified
249+
if startASAP is true
250+
type: boolean
251+
hourOfDay:
252+
description: Hour of the day when you would like the maintenance
253+
window to start. This parameter uses the 24-hour clock, where
254+
midnight is 0, noon is 12.
255+
maximum: 23
256+
minimum: 0
257+
type: integer
258+
startASAP:
259+
description: Flag indicating whether project maintenance has been
260+
directed to start immediately. Cannot be specified if defer
261+
is true
262+
type: boolean
263+
type: object
230264
name:
231265
description: Name is the name of the Project that is created in Atlas
232266
by the Operator if it doesn't exist yet.

config/samples/atlas_v1_atlasproject.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ spec:
66
name: Test Atlas Operator Project
77
projectIpAccessList:
88
- ipAddress: "192.0.2.15"
9-
comment: "IP address for Application Server A"
9+
comment: "IP address for Application Server A"
10+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apiVersion: atlas.mongodb.com/v1
2+
kind: AtlasProject
3+
metadata:
4+
name: my-project
5+
spec:
6+
name: Test Atlas Operator Project
7+
projectIpAccessList:
8+
- ipAddress: "192.0.2.15"
9+
comment: "IP address for Application Server A"
10+
maintenanceWindow:
11+
dayOfWeek: 3
12+
hourOfDay: 5
13+
autoDefer: true
14+

pkg/api/v1/atlasproject_types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ type AtlasProjectSpec struct {
5555
// +optional
5656
ProjectIPAccessList []project.IPAccessList `json:"projectIpAccessList,omitempty"`
5757

58+
// MaintenanceWindow allows to specify a preferred time in the week to run maintenance operations. See more
59+
// information at https://www.mongodb.com/docs/atlas/reference/api/maintenance-windows/
60+
// +optional
61+
MaintenanceWindow project.MaintenanceWindow `json:"maintenanceWindow,omitempty"`
62+
5863
// PrivateEndpoints is a list of Private Endpoints configured for the current Project.
5964
PrivateEndpoints []PrivateEndpoint `json:"privateEndpoints,omitempty"`
6065

@@ -170,6 +175,11 @@ func (p *AtlasProject) WithIPAccessList(ipAccess project.IPAccessList) *AtlasPro
170175
return p
171176
}
172177

178+
func (p *AtlasProject) WithMaintenanceWindow(window project.MaintenanceWindow) *AtlasProject {
179+
p.Spec.MaintenanceWindow = window
180+
return p
181+
}
182+
173183
func DefaultProject(namespace, connectionSecretName string) *AtlasProject {
174184
return NewProject(namespace, "test-project", namespace).WithConnectionSecret(connectionSecretName)
175185
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package project
2+
3+
import (
4+
"go.mongodb.org/atlas/mongodbatlas"
5+
)
6+
7+
type MaintenanceWindow struct {
8+
// Day of the week when you would like the maintenance window to start as a 1-based integer.
9+
// Sunday 1, Monday 2, Tuesday 3, Wednesday 4, Thursday 5, Friday 6, Saturday 7
10+
// +optional
11+
// +kubebuilder:validation:Minimum=1
12+
// +kubebuilder:validation:Maximum=7
13+
DayOfWeek int `json:"dayOfWeek,omitempty"`
14+
// Hour of the day when you would like the maintenance window to start.
15+
// This parameter uses the 24-hour clock, where midnight is 0, noon is 12.
16+
// +optional
17+
// +kubebuilder:validation:Minimum=0
18+
// +kubebuilder:validation:Maximum=23
19+
HourOfDay int `json:"hourOfDay,omitempty"`
20+
// Flag indicating whether any scheduled project maintenance should be deferred automatically for one week.
21+
// +optional
22+
AutoDefer bool `json:"autoDefer,omitempty"`
23+
// Flag indicating whether project maintenance has been directed to start immediately.
24+
// Cannot be specified if defer is true
25+
// +optional
26+
StartASAP bool `json:"startASAP,omitempty"`
27+
// Flag indicating whether the next scheduled project maintenance should be deferred for one week.
28+
// Cannot be specified if startASAP is true
29+
// +optional
30+
Defer bool `json:"defer,omitempty"`
31+
}
32+
33+
// ToAtlas converts the MaintenanceWindow to native Atlas client format.
34+
func (m MaintenanceWindow) ToAtlas() *mongodbatlas.MaintenanceWindow {
35+
return &mongodbatlas.MaintenanceWindow{
36+
DayOfWeek: m.DayOfWeek,
37+
HourOfDay: &m.HourOfDay,
38+
StartASAP: &m.StartASAP,
39+
NumberOfDeferrals: 0,
40+
AutoDeferOnceEnabled: &m.AutoDefer,
41+
}
42+
}
43+
44+
// ************************************ Builder methods *************************************************
45+
// Note, that we don't use pointers here as the AtlasProject uses this without pointers
46+
47+
func NewMaintenanceWindow() MaintenanceWindow {
48+
return MaintenanceWindow{}
49+
}
50+
51+
func (m MaintenanceWindow) WithDay(day int) MaintenanceWindow {
52+
m.DayOfWeek = day
53+
return m
54+
}
55+
56+
func (m MaintenanceWindow) WithHour(hour int) MaintenanceWindow {
57+
m.HourOfDay = hour
58+
return m
59+
}
60+
61+
func (m MaintenanceWindow) WithAutoDefer(autoDefer bool) MaintenanceWindow {
62+
m.AutoDefer = autoDefer
63+
return m
64+
}
65+
66+
func (m MaintenanceWindow) WithStartASAP(startASAP bool) MaintenanceWindow {
67+
m.StartASAP = startASAP
68+
return m
69+
}
70+
71+
func (m MaintenanceWindow) WithDefer(isDefer bool) MaintenanceWindow {
72+
m.Defer = isDefer
73+
return m
74+
}

pkg/api/v1/status/condition.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const (
3636
const (
3737
ProjectReadyType ConditionType = "ProjectReady"
3838
IPAccessListReadyType ConditionType = "IPAccessListReady"
39+
MaintenanceWindowReadyType ConditionType = "MaintenanceWindowReady"
3940
PrivateEndpointServiceReadyType ConditionType = "PrivateEndpointServiceReady"
4041
PrivateEndpointReadyType ConditionType = "PrivateEndpointReady"
4142
IntegrationReadyType ConditionType = "ThirdPartyIntegrationReady"

pkg/api/v1/zz_generated.deepcopy.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/controller/atlasproject/atlasproject_controller.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,14 @@ func (r *AtlasProjectReconciler) Reconcile(context context.Context, req ctrl.Req
201201
return reconcile.Result{Requeue: true}, nil
202202
}
203203

204+
if result = ensureMaintenanceWindow(ctx, projectID, project); !result.IsOk() {
205+
setCondition(ctx, status.MaintenanceWindowReadyType, result)
206+
r.Log.Warnf("Maintenance window reconciliation failed with error : %s", result.GetMessage())
207+
return result.ReconcileResult(), nil
208+
}
209+
r.EventRecorder.Event(project, "Normal", string(status.MaintenanceWindowReadyType), "")
210+
ctx.SetConditionTrue(status.MaintenanceWindowReadyType)
211+
204212
if result = r.ensurePrivateEndpoint(ctx, projectID, project); !result.IsOk() {
205213
return result.ReconcileResult(), nil
206214
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package atlasproject
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
"go.mongodb.org/atlas/mongodbatlas"
8+
9+
mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1"
10+
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/project"
11+
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/workflow"
12+
)
13+
14+
// ensureMaintenanceWindow ensures that the state of the Atlas Maintenance Window matches the
15+
// state of the Maintenance Window specified in the project CR. If a Maintenance Window exists
16+
// in Atlas but is not specified in the CR, it is deleted.
17+
func ensureMaintenanceWindow(ctx *workflow.Context, projectID string, atlasProject *mdbv1.AtlasProject) workflow.Result {
18+
windowSpec := atlasProject.Spec.MaintenanceWindow
19+
if err := validateMaintenanceWindow(windowSpec); err != nil {
20+
return workflow.Terminate(workflow.ProjectWindowInvalid, err.Error())
21+
}
22+
23+
if isEmptyWindow(windowSpec) {
24+
ctx.Log.Debugw("Window empty or undefined, deleting in Atlas")
25+
if result := deleteInAtlas(ctx.Client, projectID); !result.IsOk() {
26+
return result
27+
}
28+
return workflow.OK()
29+
}
30+
31+
if windowSpecified(windowSpec) {
32+
ctx.Log.Debugw("Checking if window needs update")
33+
windowInAtlas, result := getInAtlas(ctx.Client, projectID)
34+
if !result.IsOk() {
35+
return result
36+
}
37+
38+
if windowInAtlas.DayOfWeek == 0 || windowInAtlas.HourOfDay == nil ||
39+
*windowInAtlas.HourOfDay != windowSpec.HourOfDay || windowInAtlas.DayOfWeek != windowSpec.DayOfWeek {
40+
ctx.Log.Debugw("Creating or updating window")
41+
// We set startASAP to false because the operator takes care of calling the API a second time if both
42+
// startASAP and the new maintenance timeslots are defined
43+
if result := createOrUpdateInAtlas(ctx.Client, projectID, windowSpec.WithStartASAP(false)); !result.IsOk() {
44+
return result
45+
}
46+
} else if *windowInAtlas.AutoDeferOnceEnabled != windowSpec.AutoDefer {
47+
// If autoDefer flag is different in Atlas, and we haven't updated the window previously, we toggle the flag
48+
ctx.Log.Debugw("Toggling autoDefer")
49+
if result := toggleAutoDeferInAtlas(ctx.Client, projectID); !result.IsOk() {
50+
return result
51+
}
52+
}
53+
}
54+
55+
if windowSpec.StartASAP {
56+
ctx.Log.Debugw("Starting maintenance ASAP")
57+
// To avoid any unexpected behavior, we send a request to the API containing only the StartASAP flag,
58+
// although the API should ignore other fields in that case
59+
if result := createOrUpdateInAtlas(ctx.Client, projectID, project.NewMaintenanceWindow().WithStartASAP(true)); !result.IsOk() {
60+
return result
61+
}
62+
// Nothing else should be done after sending a StartASAP request
63+
return workflow.OK()
64+
}
65+
66+
if windowSpec.Defer {
67+
ctx.Log.Debugw("Deferring scheduled maintenance")
68+
if result := deferInAtlas(ctx.Client, projectID); !result.IsOk() {
69+
return result
70+
}
71+
// Nothing else should be done after deferring
72+
return workflow.OK()
73+
}
74+
75+
return workflow.OK()
76+
}
77+
78+
func isEmpty(i int) bool {
79+
return i == 0
80+
}
81+
82+
func isEmptyWindow(window project.MaintenanceWindow) bool {
83+
return isEmpty(window.DayOfWeek) && isEmpty(window.HourOfDay) && !window.StartASAP && !window.Defer && !window.AutoDefer
84+
}
85+
86+
func windowSpecified(window project.MaintenanceWindow) bool {
87+
return !isEmpty(window.DayOfWeek)
88+
}
89+
90+
func maxOneFlag(window project.MaintenanceWindow) bool {
91+
return !(window.StartASAP && window.Defer)
92+
}
93+
94+
// validateMaintenanceWindow performs validation of the Maintenance Window. Note, that we intentionally don't validate
95+
// that hour of day and day of week are in the bounds - this will be done by Atlas.
96+
func validateMaintenanceWindow(window project.MaintenanceWindow) error {
97+
if isEmptyWindow(window) || (windowSpecified(window) && maxOneFlag(window)) {
98+
return nil
99+
}
100+
errorString := "projectMaintenanceWindow must respect the following constraints, or be empty : " +
101+
"1) dayOfWeek must be specified (hourOfDay is 0 by default, autoDeferral is false by default) " +
102+
"2) only one of (startASAP, defer) is true"
103+
return errors.New(errorString)
104+
}
105+
106+
// operatorToAtlasMaintenanceWindow converts the maintenanceWindow specified in the project CR to the format
107+
// expected by the Atlas API.
108+
func operatorToAtlasMaintenanceWindow(maintenanceWindow project.MaintenanceWindow) (*mongodbatlas.MaintenanceWindow, workflow.Result) {
109+
operatorWindow := maintenanceWindow.ToAtlas()
110+
return operatorWindow, workflow.OK()
111+
}
112+
113+
func getInAtlas(client mongodbatlas.Client, projectID string) (*mongodbatlas.MaintenanceWindow, workflow.Result) {
114+
window, _, err := client.MaintenanceWindows.Get(context.Background(), projectID)
115+
if err != nil {
116+
return nil, workflow.Terminate(workflow.ProjectWindowNotObtainedFromAtlas, err.Error())
117+
}
118+
return window, workflow.OK()
119+
}
120+
121+
func createOrUpdateInAtlas(client mongodbatlas.Client, projectID string, maintenanceWindow project.MaintenanceWindow) workflow.Result {
122+
operatorWindow, status := operatorToAtlasMaintenanceWindow(maintenanceWindow)
123+
if !status.IsOk() {
124+
return status
125+
}
126+
127+
if _, err := client.MaintenanceWindows.Update(context.Background(), projectID, operatorWindow); err != nil {
128+
return workflow.Terminate(workflow.ProjectWindowNotCreatedInAtlas, err.Error())
129+
}
130+
return workflow.OK()
131+
}
132+
133+
func deleteInAtlas(client mongodbatlas.Client, projectID string) workflow.Result {
134+
if _, err := client.MaintenanceWindows.Reset(context.Background(), projectID); err != nil {
135+
return workflow.Terminate(workflow.ProjectWindowNotDeletedInAtlas, err.Error())
136+
}
137+
return workflow.OK()
138+
}
139+
140+
func deferInAtlas(client mongodbatlas.Client, projectID string) workflow.Result {
141+
if _, err := client.MaintenanceWindows.Defer(context.Background(), projectID); err != nil {
142+
return workflow.Terminate(workflow.ProjectWindowNotDeferredInAtlas, err.Error())
143+
}
144+
return workflow.OK()
145+
}
146+
147+
// toggleAutoDeferInAtlas toggles the field "autoDeferOnceEnabled" by sending a POST /autoDefer request to the API
148+
func toggleAutoDeferInAtlas(client mongodbatlas.Client, projectID string) workflow.Result {
149+
if _, err := client.MaintenanceWindows.AutoDefer(context.Background(), projectID); err != nil {
150+
return workflow.Terminate(workflow.ProjectWindowNotAutoDeferredInAtlas, err.Error())
151+
}
152+
return workflow.OK()
153+
}

0 commit comments

Comments
 (0)