@@ -25,6 +25,7 @@ import (
2525 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2626 "k8s.io/apimachinery/pkg/runtime"
2727 utilruntime "k8s.io/apimachinery/pkg/util/runtime"
28+ "k8s.io/utils/pointer"
2829 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
2930 clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
3031 "sigs.k8s.io/controller-runtime/pkg/client"
@@ -462,6 +463,181 @@ func TestNewGenericProviderList(t *testing.T) {
462463 }
463464}
464465
466+ func TestProviderSpecChanges (t * testing.T ) {
467+ testCases := []struct {
468+ name string
469+ spec operatorv1.ProviderSpec
470+ updatedSpec operatorv1.ProviderSpec
471+ expectError bool
472+ }{
473+ {
474+ name : "same spec, hash annotation doesn't change" ,
475+ spec : operatorv1.ProviderSpec {
476+ Version : testCurrentVersion ,
477+ FetchConfig : & operatorv1.FetchConfiguration {
478+ Selector : & metav1.LabelSelector {
479+ MatchLabels : map [string ]string {
480+ "test" : "dummy-config" ,
481+ },
482+ },
483+ },
484+ },
485+ updatedSpec : operatorv1.ProviderSpec {
486+ Version : testCurrentVersion ,
487+ FetchConfig : & operatorv1.FetchConfiguration {
488+ Selector : & metav1.LabelSelector {
489+ MatchLabels : map [string ]string {
490+ "test" : "dummy-config" ,
491+ },
492+ },
493+ },
494+ },
495+ },
496+ {
497+ name : "add more replicas, hash annotation is updated" ,
498+ spec : operatorv1.ProviderSpec {
499+ Version : testCurrentVersion ,
500+ FetchConfig : & operatorv1.FetchConfiguration {
501+ Selector : & metav1.LabelSelector {
502+ MatchLabels : map [string ]string {
503+ "test" : "dummy-config" ,
504+ },
505+ },
506+ },
507+ },
508+ updatedSpec : operatorv1.ProviderSpec {
509+ Version : testCurrentVersion ,
510+ Deployment : & operatorv1.DeploymentSpec {
511+ Replicas : pointer .Int (2 ),
512+ },
513+ FetchConfig : & operatorv1.FetchConfiguration {
514+ Selector : & metav1.LabelSelector {
515+ MatchLabels : map [string ]string {
516+ "test" : "dummy-config" ,
517+ },
518+ },
519+ },
520+ },
521+ },
522+ {
523+ name : "upgrade to a non-existent version, hash annotation is empty" ,
524+ expectError : true ,
525+ spec : operatorv1.ProviderSpec {
526+ Version : testCurrentVersion ,
527+ FetchConfig : & operatorv1.FetchConfiguration {
528+ Selector : & metav1.LabelSelector {
529+ MatchLabels : map [string ]string {
530+ "test" : "dummy-config" ,
531+ },
532+ },
533+ },
534+ },
535+ updatedSpec : operatorv1.ProviderSpec {
536+ Version : "10000.0.0-NONEXISTENT" ,
537+ Deployment : & operatorv1.DeploymentSpec {
538+ Replicas : pointer .Int (2 ),
539+ },
540+ FetchConfig : & operatorv1.FetchConfiguration {
541+ Selector : & metav1.LabelSelector {
542+ MatchLabels : map [string ]string {
543+ "test" : "dummy-config" ,
544+ },
545+ },
546+ },
547+ },
548+ },
549+ }
550+
551+ for _ , tc := range testCases {
552+ t .Run (tc .name , func (t * testing.T ) {
553+ g := NewWithT (t )
554+
555+ specHash , err := calculateHash (tc .spec )
556+ g .Expect (err ).ToNot (HaveOccurred ())
557+
558+ updatedSpecHash , err := calculateHash (tc .spec )
559+ g .Expect (err ).ToNot (HaveOccurred ())
560+
561+ provider := & genericprovider.CoreProviderWrapper {
562+ CoreProvider : & operatorv1.CoreProvider {
563+ ObjectMeta : metav1.ObjectMeta {
564+ Name : "cluster-api" ,
565+ },
566+ Spec : operatorv1.CoreProviderSpec {
567+ ProviderSpec : tc .spec ,
568+ },
569+ },
570+ }
571+
572+ namespace := "test-provider-spec-changes"
573+
574+ t .Log ("Ensure namespace exists" , namespace )
575+ g .Expect (env .EnsureNamespaceExists (ctx , namespace )).To (Succeed ())
576+
577+ g .Expect (env .CreateAndWait (ctx , dummyConfigMap (namespace , testCurrentVersion ))).To (Succeed ())
578+
579+ provider .SetNamespace (namespace )
580+ t .Log ("creating test provider" , provider .GetName ())
581+ g .Expect (env .CreateAndWait (ctx , provider .GetObject ())).To (Succeed ())
582+
583+ g .Eventually (generateExpectedResultChecker (provider , specHash , corev1 .ConditionTrue ), timeout ).Should (BeEquivalentTo (true ))
584+
585+ // Change provider spec
586+ provider .SetSpec (tc .updatedSpec )
587+
588+ // Set a label to ensure that provider was changed
589+ labels := provider .GetLabels ()
590+ if labels == nil {
591+ labels = map [string ]string {}
592+ }
593+ labels ["my-label" ] = "some-value"
594+ provider .SetLabels (labels )
595+
596+ g .Expect (env .Client .Update (ctx , provider .GetObject ())).To (Succeed ())
597+
598+ if ! tc .expectError {
599+ g .Eventually (generateExpectedResultChecker (provider , updatedSpecHash , corev1 .ConditionTrue ), timeout ).Should (BeEquivalentTo (true ))
600+ } else {
601+ g .Eventually (generateExpectedResultChecker (provider , "" , corev1 .ConditionFalse ), timeout ).Should (BeEquivalentTo (true ))
602+ }
603+
604+ // Clean up
605+ objs := []client.Object {provider .GetObject ()}
606+ objs = append (objs , & corev1.ConfigMap {
607+ ObjectMeta : metav1.ObjectMeta {
608+ Name : testCurrentVersion ,
609+ Namespace : namespace ,
610+ },
611+ })
612+
613+ g .Expect (env .CleanupAndWait (ctx , objs ... )).To (Succeed ())
614+ })
615+ }
616+ }
617+
618+ func generateExpectedResultChecker (provider * genericprovider.CoreProviderWrapper , specHash string , condStatus corev1.ConditionStatus ) func () bool {
619+ return func () bool {
620+ if err := env .Get (ctx , client .ObjectKeyFromObject (provider .GetObject ()), provider .GetObject ()); err != nil {
621+ return false
622+ }
623+
624+ // In case of error we don't want the spec annotation to be updated
625+ if provider .GetAnnotations ()[appliedSpecHashAnnotation ] != specHash {
626+ return false
627+ }
628+
629+ for _ , cond := range provider .GetStatus ().Conditions {
630+ if cond .Type == operatorv1 .ProviderInstalledCondition {
631+ if cond .Status == condStatus {
632+ return true
633+ }
634+ }
635+ }
636+
637+ return false
638+ }
639+ }
640+
465641func setupScheme () * runtime.Scheme {
466642 scheme := runtime .NewScheme ()
467643 utilruntime .Must (corev1 .AddToScheme (scheme ))
0 commit comments