@@ -3,9 +3,13 @@ package upgrade
33import (
44 "context"
55 "encoding/json"
6+ "fmt"
7+ "sync/atomic"
68 "testing"
79
10+ apv1b2 "github.com/k0sproject/k0s/pkg/apis/autopilot/v1beta2"
811 k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1"
12+ "github.com/k0sproject/k0s/pkg/autopilot/controller/plans/core"
913 ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1"
1014 "github.com/sirupsen/logrus"
1115 "github.com/stretchr/testify/assert"
@@ -369,3 +373,149 @@ config:
369373 })
370374 }
371375}
376+
377+ func TestWaitForAutopilotPlan_Success (t * testing.T ) {
378+ logger := logrus .New ()
379+ logger .SetLevel (logrus .ErrorLevel )
380+
381+ scheme := runtime .NewScheme ()
382+ require .NoError (t , apv1b2 .Install (scheme ))
383+
384+ plan := & apv1b2.Plan {
385+ ObjectMeta : metav1.ObjectMeta {
386+ Name : "autopilot" ,
387+ },
388+ Status : apv1b2.PlanStatus {
389+ State : core .PlanCompleted ,
390+ },
391+ }
392+
393+ cli := fake .NewClientBuilder ().
394+ WithScheme (scheme ).
395+ WithObjects (plan ).
396+ Build ()
397+
398+ result , err := waitForAutopilotPlan (t .Context (), cli , logger )
399+ require .NoError (t , err )
400+ assert .Equal (t , "autopilot" , result .Name )
401+ }
402+
403+ func TestWaitForAutopilotPlan_RetriesOnTransientErrors (t * testing.T ) {
404+ logger := logrus .New ()
405+ logger .SetLevel (logrus .ErrorLevel )
406+
407+ scheme := runtime .NewScheme ()
408+ require .NoError (t , apv1b2 .Install (scheme ))
409+
410+ // Plan that starts completed
411+ plan := & apv1b2.Plan {
412+ ObjectMeta : metav1.ObjectMeta {
413+ Name : "autopilot" ,
414+ },
415+ Status : apv1b2.PlanStatus {
416+ State : core .PlanCompleted ,
417+ },
418+ }
419+
420+ // Mock client that fails first 3 times, then succeeds
421+ var callCount atomic.Int32
422+ cli := & mockClientWithRetries {
423+ Client : fake .NewClientBuilder ().WithScheme (scheme ).WithObjects (plan ).Build (),
424+ failCount : 3 ,
425+ currentCount : & callCount ,
426+ }
427+
428+ result , err := waitForAutopilotPlan (t .Context (), cli , logger )
429+ require .NoError (t , err )
430+ assert .Equal (t , "autopilot" , result .Name )
431+ assert .Equal (t , int32 (4 ), callCount .Load (), "Should have retried 3 times before succeeding" )
432+ }
433+
434+ func TestWaitForAutopilotPlan_ContextCanceled (t * testing.T ) {
435+ logger := logrus .New ()
436+ logger .SetLevel (logrus .ErrorLevel )
437+
438+ scheme := runtime .NewScheme ()
439+ require .NoError (t , apv1b2 .Install (scheme ))
440+
441+ ctx , cancel := context .WithCancel (t .Context ())
442+ cancel () // Cancel immediately
443+
444+ cli := fake .NewClientBuilder ().WithScheme (scheme ).Build ()
445+
446+ _ , err := waitForAutopilotPlan (ctx , cli , logger )
447+ require .Error (t , err )
448+ assert .Contains (t , err .Error (), "context canceled" )
449+ }
450+
451+ func TestWaitForAutopilotPlan_WaitsForCompletion (t * testing.T ) {
452+ logger := logrus .New ()
453+ logger .SetLevel (logrus .ErrorLevel )
454+
455+ scheme := runtime .NewScheme ()
456+ require .NoError (t , apv1b2 .Install (scheme ))
457+
458+ // Plan that starts in progress, then completes after some time
459+ plan := & apv1b2.Plan {
460+ ObjectMeta : metav1.ObjectMeta {
461+ Name : "autopilot" ,
462+ },
463+ Spec : apv1b2.PlanSpec {
464+ ID : "test-plan" ,
465+ },
466+ Status : apv1b2.PlanStatus {
467+ State : core .PlanSchedulable ,
468+ },
469+ }
470+
471+ cli := & mockClientWithStateChange {
472+ Client : fake .NewClientBuilder ().WithScheme (scheme ).WithObjects (plan ).Build (),
473+ plan : plan ,
474+ callsUntil : 3 , // Will complete after 3 calls
475+ }
476+
477+ result , err := waitForAutopilotPlan (t .Context (), cli , logger )
478+ require .NoError (t , err )
479+ assert .Equal (t , "autopilot" , result .Name )
480+ assert .Equal (t , core .PlanCompleted , result .Status .State )
481+ }
482+
483+ // Mock client that fails N times before succeeding
484+ type mockClientWithRetries struct {
485+ client.Client
486+ failCount int
487+ currentCount * atomic.Int32
488+ }
489+
490+ func (m * mockClientWithRetries ) Get (ctx context.Context , key client.ObjectKey , obj client.Object , opts ... client.GetOption ) error {
491+ count := m .currentCount .Add (1 )
492+ if count <= int32 (m .failCount ) {
493+ return fmt .Errorf ("connection refused" )
494+ }
495+ return m .Client .Get (ctx , key , obj , opts ... )
496+ }
497+
498+ // Mock client that changes plan state after N calls
499+ type mockClientWithStateChange struct {
500+ client.Client
501+ plan * apv1b2.Plan
502+ callCount int
503+ callsUntil int
504+ }
505+
506+ func (m * mockClientWithStateChange ) Get (ctx context.Context , key client.ObjectKey , obj client.Object , opts ... client.GetOption ) error {
507+ m .callCount ++
508+ err := m .Client .Get (ctx , key , obj , opts ... )
509+ if err != nil {
510+ return err
511+ }
512+
513+ // After N calls, mark the plan as completed
514+ if m .callCount >= m .callsUntil {
515+ if plan , ok := obj .(* apv1b2.Plan ); ok {
516+ plan .Status .State = core .PlanCompleted
517+ }
518+ }
519+
520+ return nil
521+ }
0 commit comments