@@ -51,6 +51,7 @@ var _ = Describe("Hypervisor Spec CEL Validation", func() {
5151 EvacuateOnReboot : true ,
5252 InstallCertificate : true ,
5353 Maintenance : MaintenanceManual ,
54+ MaintenanceReason : "Test maintenance reason" ,
5455 },
5556 }
5657
@@ -204,3 +205,194 @@ var _ = Describe("Maintenance Constants", func() {
204205 Expect (MaintenanceTermination ).To (Equal ("termination" ))
205206 })
206207})
208+
209+ // TestMaintenanceReasonValidation tests the CEL validation rule for MaintenanceReason
210+ // The rule: !has(self.maintenance) || self.maintenance != 'manual' || (has(self.maintenanceReason) && self.maintenanceReason != ”)
211+ // This ensures that when maintenance is set to 'manual', maintenanceReason must be non-empty
212+ var _ = Describe ("MaintenanceReason CEL Validation" , func () {
213+ var (
214+ hypervisor * Hypervisor
215+ hypervisorName types.NamespacedName
216+ )
217+
218+ Context ("When creating a new Hypervisor" , func () {
219+ AfterEach (func (ctx SpecContext ) {
220+ if hypervisor != nil {
221+ Expect (client .IgnoreNotFound (k8sClient .Delete (ctx , hypervisor ))).To (Succeed ())
222+ }
223+ })
224+
225+ It ("should allow creation with maintenance='manual' and a non-empty maintenanceReason" , func (ctx SpecContext ) {
226+ hypervisor = & Hypervisor {
227+ ObjectMeta : metav1.ObjectMeta {
228+ Name : "test-hypervisor-manual-with-reason" ,
229+ },
230+ Spec : HypervisorSpec {
231+ OperatingSystemVersion : "1.0" ,
232+ LifecycleEnabled : true ,
233+ Maintenance : MaintenanceManual ,
234+ MaintenanceReason : "Hardware upgrade required" ,
235+ },
236+ }
237+
238+ Expect (k8sClient .Create (ctx , hypervisor )).To (Succeed ())
239+
240+ created := & Hypervisor {}
241+ Expect (k8sClient .Get (ctx , types.NamespacedName {Name : hypervisor .Name }, created )).To (Succeed ())
242+ Expect (created .Spec .Maintenance ).To (Equal (MaintenanceManual ))
243+ Expect (created .Spec .MaintenanceReason ).To (Equal ("Hardware upgrade required" ))
244+ })
245+
246+ It ("should reject creation with maintenance='manual' but empty maintenanceReason" , func (ctx SpecContext ) {
247+ hypervisor = & Hypervisor {
248+ ObjectMeta : metav1.ObjectMeta {
249+ Name : "test-hypervisor-manual-empty-reason" ,
250+ },
251+ Spec : HypervisorSpec {
252+ Maintenance : MaintenanceManual ,
253+ MaintenanceReason : "" ,
254+ },
255+ }
256+
257+ err := k8sClient .Create (ctx , hypervisor )
258+ Expect (err ).To (HaveOccurred ())
259+ Expect (err .Error ()).To (ContainSubstring ("maintenanceReason must be non-empty when maintenance is 'manual'" ))
260+ })
261+
262+ It ("should reject creation with maintenance='manual' but missing maintenanceReason" , func (ctx SpecContext ) {
263+ hypervisor = & Hypervisor {
264+ ObjectMeta : metav1.ObjectMeta {
265+ Name : "test-hypervisor-manual-no-reason" ,
266+ },
267+ Spec : HypervisorSpec {
268+ Maintenance : MaintenanceManual ,
269+ },
270+ }
271+
272+ err := k8sClient .Create (ctx , hypervisor )
273+ Expect (err ).To (HaveOccurred ())
274+ Expect (err .Error ()).To (ContainSubstring ("maintenanceReason must be non-empty when maintenance is 'manual'" ))
275+ })
276+
277+ It ("should allow creation with non-manual maintenance modes without maintenanceReason" , func (ctx SpecContext ) {
278+ hypervisor = & Hypervisor {
279+ ObjectMeta : metav1.ObjectMeta {
280+ Name : "test-hypervisor-auto-no-reason" ,
281+ },
282+ Spec : HypervisorSpec {
283+ Maintenance : MaintenanceAuto ,
284+ },
285+ }
286+
287+ Expect (k8sClient .Create (ctx , hypervisor )).To (Succeed ())
288+
289+ created := & Hypervisor {}
290+ Expect (k8sClient .Get (ctx , types.NamespacedName {Name : hypervisor .Name }, created )).To (Succeed ())
291+ Expect (created .Spec .Maintenance ).To (Equal (MaintenanceAuto ))
292+ })
293+
294+ It ("should allow creation with empty maintenance and no maintenanceReason" , func (ctx SpecContext ) {
295+ hypervisor = & Hypervisor {
296+ ObjectMeta : metav1.ObjectMeta {
297+ Name : "test-hypervisor-no-maintenance" ,
298+ },
299+ Spec : HypervisorSpec {
300+ LifecycleEnabled : true ,
301+ },
302+ }
303+
304+ Expect (k8sClient .Create (ctx , hypervisor )).To (Succeed ())
305+
306+ created := & Hypervisor {}
307+ Expect (k8sClient .Get (ctx , types.NamespacedName {Name : hypervisor .Name }, created )).To (Succeed ())
308+ Expect (created .Spec .Maintenance ).To (Equal ("" ))
309+ })
310+ })
311+
312+ Context ("When updating an existing Hypervisor" , func () {
313+ BeforeEach (func (ctx SpecContext ) {
314+ hypervisorName = types.NamespacedName {
315+ Name : "test-hypervisor-update" ,
316+ }
317+
318+ hypervisor = & Hypervisor {
319+ ObjectMeta : metav1.ObjectMeta {
320+ Name : hypervisorName .Name ,
321+ },
322+ Spec : HypervisorSpec {
323+ LifecycleEnabled : true ,
324+ Maintenance : MaintenanceAuto ,
325+ },
326+ }
327+
328+ Expect (k8sClient .Create (ctx , hypervisor )).To (Succeed ())
329+ })
330+
331+ AfterEach (func (ctx SpecContext ) {
332+ Expect (client .IgnoreNotFound (k8sClient .Delete (ctx , hypervisor ))).To (Succeed ())
333+ })
334+
335+ It ("should allow updating to maintenance='manual' with a non-empty maintenanceReason" , func (ctx SpecContext ) {
336+ hypervisor .Spec .Maintenance = MaintenanceManual
337+ hypervisor .Spec .MaintenanceReason = "Planned maintenance window"
338+ Expect (k8sClient .Update (ctx , hypervisor )).To (Succeed ())
339+
340+ updated := & Hypervisor {}
341+ Expect (k8sClient .Get (ctx , hypervisorName , updated )).To (Succeed ())
342+ Expect (updated .Spec .Maintenance ).To (Equal (MaintenanceManual ))
343+ Expect (updated .Spec .MaintenanceReason ).To (Equal ("Planned maintenance window" ))
344+ })
345+
346+ It ("should reject updating to maintenance='manual' without maintenanceReason" , func (ctx SpecContext ) {
347+ hypervisor .Spec .Maintenance = MaintenanceManual
348+ err := k8sClient .Update (ctx , hypervisor )
349+ Expect (err ).To (HaveOccurred ())
350+ Expect (err .Error ()).To (ContainSubstring ("maintenanceReason must be non-empty when maintenance is 'manual'" ))
351+ })
352+
353+ It ("should reject updating to maintenance='manual' with empty maintenanceReason" , func (ctx SpecContext ) {
354+ hypervisor .Spec .Maintenance = MaintenanceManual
355+ hypervisor .Spec .MaintenanceReason = ""
356+ err := k8sClient .Update (ctx , hypervisor )
357+ Expect (err ).To (HaveOccurred ())
358+ Expect (err .Error ()).To (ContainSubstring ("maintenanceReason must be non-empty when maintenance is 'manual'" ))
359+ })
360+
361+ It ("should allow updating from manual to another maintenance mode" , func (ctx SpecContext ) {
362+ // First set to manual with reason
363+ hypervisor .Spec .Maintenance = MaintenanceManual
364+ hypervisor .Spec .MaintenanceReason = "Initial reason"
365+ Expect (k8sClient .Update (ctx , hypervisor )).To (Succeed ())
366+
367+ // Refresh hypervisor
368+ Expect (k8sClient .Get (ctx , hypervisorName , hypervisor )).To (Succeed ())
369+
370+ // Update to auto (reason becomes optional and can be cleared)
371+ hypervisor .Spec .Maintenance = MaintenanceAuto
372+ hypervisor .Spec .MaintenanceReason = ""
373+ Expect (k8sClient .Update (ctx , hypervisor )).To (Succeed ())
374+
375+ updated := & Hypervisor {}
376+ Expect (k8sClient .Get (ctx , hypervisorName , updated )).To (Succeed ())
377+ Expect (updated .Spec .Maintenance ).To (Equal (MaintenanceAuto ))
378+ })
379+
380+ It ("should allow updating maintenanceReason when maintenance is already 'manual'" , func (ctx SpecContext ) {
381+ // First set to manual with reason
382+ hypervisor .Spec .Maintenance = MaintenanceManual
383+ hypervisor .Spec .MaintenanceReason = "Initial reason"
384+ Expect (k8sClient .Update (ctx , hypervisor )).To (Succeed ())
385+
386+ // Refresh hypervisor
387+ Expect (k8sClient .Get (ctx , hypervisorName , hypervisor )).To (Succeed ())
388+
389+ // Update the reason
390+ hypervisor .Spec .MaintenanceReason = "Updated reason"
391+ Expect (k8sClient .Update (ctx , hypervisor )).To (Succeed ())
392+
393+ updated := & Hypervisor {}
394+ Expect (k8sClient .Get (ctx , hypervisorName , updated )).To (Succeed ())
395+ Expect (updated .Spec .MaintenanceReason ).To (Equal ("Updated reason" ))
396+ })
397+ })
398+ })
0 commit comments