55 "encoding/json"
66 "fmt"
77 "sort"
8+ "strconv"
89 "strings"
910
1011 "github.com/google/go-cmp/cmp"
7273 // relatedObjectsCRDs is a set of names of CRDs that we add to
7374 // relatedObjects if they exist.
7475 relatedObjectsCRDs = sets .New [string ](gatewaysResourceName , gatewayclassesResourceName , istiosResourceName )
76+
77+ // ossmSubscriptions lists the package names for all OLM subscriptions that are known to conflict with the
78+ // subscription to OSSM3 created by the Ingress Operator.
79+ ossmSubscriptions = sets .New [string ](
80+ "sailoperator" ,
81+ "servicemeshoperator" ,
82+ "servicemeshoperator3" ,
83+ )
7584)
7685
7786// New creates the status controller. This is the controller that handles all
@@ -83,10 +92,20 @@ var (
8392// in case something else updates or deletes it.
8493func New (mgr manager.Manager , config Config ) (controller.Controller , error ) {
8594 operatorCache := mgr .GetCache ()
95+ // The status controller needs to be aware of conflicting OLM subscriptions in any namespace. In order to
96+ // prevent ballooning the main cache to watch all namespaces on all its informers, create a separate cache for
97+ // tracking OLM subscriptions across all namespaces.
98+ var subscriptionCache cache.Cache
99+ var err error
100+ if subscriptionCache , err = cache .New (mgr .GetConfig (), cache.Options {}); err != nil {
101+ return nil , err
102+ }
103+ mgr .Add (subscriptionCache )
86104 reconciler := & reconciler {
87- config : config ,
88- client : mgr .GetClient (),
89- cache : operatorCache ,
105+ config : config ,
106+ client : mgr .GetClient (),
107+ cache : operatorCache ,
108+ subscriptionCache : subscriptionCache ,
90109 }
91110 c , err := controller .New (controllerName , mgr , controller.Options {Reconciler : reconciler })
92111 if err != nil {
@@ -156,6 +175,17 @@ func New(mgr manager.Manager, config Config) (controller.Controller, error) {
156175 })); err != nil {
157176 return nil , err
158177 }
178+
179+ isOSSMSubscription := predicate .NewPredicateFuncs (func (o client.Object ) bool {
180+ subscription , ok := o .(* operatorsv1alpha1.Subscription )
181+ if ! ok || subscription .Spec == nil {
182+ return false
183+ }
184+ return ossmSubscriptions .Has (subscription .Spec .Package )
185+ })
186+ if err := c .Watch (source .Kind [client.Object ](reconciler .subscriptionCache , & operatorsv1alpha1.Subscription {}, handler .EnqueueRequestsFromMapFunc (toDefaultIngressController ), isOSSMSubscription )); err != nil {
187+ return nil , err
188+ }
159189 }
160190
161191 return c , nil
@@ -178,6 +208,7 @@ type Config struct {
178208 CanaryImage string
179209 OperatorReleaseVersion string
180210 Namespace string
211+ GatewayAPIOperatorVersion string
181212}
182213
183214// IngressOperatorStatusExtension holds status extensions of the ingress cluster operator.
@@ -192,8 +223,9 @@ type IngressOperatorStatusExtension struct {
192223type reconciler struct {
193224 config Config
194225
195- client client.Client
196- cache cache.Cache
226+ client client.Client
227+ cache cache.Cache
228+ subscriptionCache cache.Cache
197229}
198230
199231// Reconcile computes the operator's current status and therefrom creates or
@@ -366,6 +398,14 @@ type operatorState struct {
366398 // haveGatewayclassesResource means that the
367399 // "gatewayclasses.gateway.networking.k8s.io" CRD exists.
368400 haveGatewayclassesResource bool
401+ // ossmSubscriptions contains all subscriptions that may conflict with the operator-created ossm subscription.
402+ ossmSubscriptions []operatorsv1alpha1.Subscription
403+ // expectedGatewayAPIOperatorVersion reflects the expected OSSM 3 version. It is used in determining if a
404+ // user-supplied OSSM 3 subscription would cause the operator's installation of OSSM 3 to fail.
405+ expectedGatewayAPIOperatorVersion string
406+ // shouldInstallOSSM reflects whether the ingress operator should install OSSM. Currently, this happens when a
407+ // gateway class with Spec.ControllerName=operatorcontroller.OpenShiftGatewayClassControllerName is created.
408+ shouldInstallOSSM bool
369409}
370410
371411// getOperatorState gets and returns the resources necessary to compute the
@@ -447,6 +487,27 @@ func (r *reconciler) getOperatorState(ctx context.Context, ingressNamespace, can
447487 } else {
448488 state .haveIstiosResource = true
449489 }
490+
491+ state .expectedGatewayAPIOperatorVersion = r .config .GatewayAPIOperatorVersion
492+ subscriptionList := operatorsv1alpha1.SubscriptionList {}
493+ if err := r .subscriptionCache .List (ctx , & subscriptionList ); err != nil {
494+ return state , fmt .Errorf ("failed to get subscriptions: %w" , err )
495+ }
496+ for _ , subscription := range subscriptionList .Items {
497+ if subscription .Spec != nil && ossmSubscriptions .Has (subscription .Spec .Package ) {
498+ state .ossmSubscriptions = append (state .ossmSubscriptions , subscription )
499+ }
500+ }
501+
502+ gatewayClassList := gatewayapiv1.GatewayClassList {}
503+ if err := r .cache .List (ctx , & gatewayClassList , client.MatchingFields {
504+ operatorcontroller .GatewayClassIndexFieldName : operatorcontroller .OpenShiftGatewayClassControllerName ,
505+ }); err != nil {
506+ return state , fmt .Errorf ("failed to list gateway classes: %w" , err )
507+ }
508+ // If one or more gateway classes have ControllerName=operatorcontroller.OpenShiftGatewayClassControllerName,
509+ // the ingress operator should try to install OSSM.
510+ state .shouldInstallOSSM = (len (gatewayClassList .Items ) > 0 )
450511 }
451512 }
452513
@@ -504,6 +565,7 @@ func computeOperatorDegradedCondition(state operatorState) configv1.ClusterOpera
504565 for _ , fn := range []func (state operatorState ) configv1.ClusterOperatorStatusCondition {
505566 computeIngressControllerDegradedCondition ,
506567 computeGatewayAPICRDsDegradedCondition ,
568+ computeGatewayAPIInstallDegradedCondition ,
507569 } {
508570 degradedCondition = joinConditions (degradedCondition , fn (state ))
509571 }
@@ -570,6 +632,64 @@ func computeGatewayAPICRDsDegradedCondition(state operatorState) configv1.Cluste
570632 return degradedCondition
571633}
572634
635+ // computeGatewayAPIInstallDegradedCondition computes the degraded condition for the Gateway API OSSM subscription. It
636+ // checks for known conflicting subscriptions, as well as already existing subscription(s) to OSSM3, and reports
637+ // degraded when any of those subscriptions would prevent the installation of an appropriate Istio control plane.
638+ func computeGatewayAPIInstallDegradedCondition (state operatorState ) configv1.ClusterOperatorStatusCondition {
639+ degradedCondition := configv1.ClusterOperatorStatusCondition {}
640+
641+ // If OSSM doesn't need to be installed, or there are no possible conflicting subscriptions, return degraded=false.
642+ if ! state .shouldInstallOSSM || len (state .ossmSubscriptions ) == 0 {
643+ return degradedCondition
644+ }
645+
646+ conflicts := []string {}
647+ warnings := []string {}
648+ for _ , subscription := range state .ossmSubscriptions {
649+ if subscription .Spec .Package == "servicemeshoperator3" {
650+ if _ , found := subscription .Annotations [operatorcontroller .IngressOperatorOwnedAnnotation ]; found {
651+ // The subscription that the ingress operator creates naturally does not conflict with itself.
652+ continue
653+ }
654+ if subscription .Status .InstalledCSV == "" {
655+ // The subscription hasn't finished its install. We will get another reconcile request once the
656+ // installation is complete, so we can ignore this for now.
657+ continue
658+ }
659+ versionDiff , err := compareVersionNums (subscription .Status .InstalledCSV , state .expectedGatewayAPIOperatorVersion )
660+ switch {
661+ case err != nil :
662+ warnings = append (warnings , fmt .Sprintf ("failed to compare installed OSSM version to expected: %v" , err ))
663+ case versionDiff < 0 :
664+ // Installed version is newer than expected. Gateway API install may still work if the correct Istio
665+ // version is supported. Warn the user that the installed OSSM version may be incompatible.
666+ warnings = append (warnings , fmt .Sprintf ("Found version %s, but operator-managed Gateway API expects version %s. Operator-managed Gateway API may not work as intended." , subscription .Status .InstalledCSV , state .expectedGatewayAPIOperatorVersion ))
667+ case versionDiff > 0 :
668+ // Installed version is older than expected. Gateway API install will not work, since the correct Istio
669+ // version won't be supported.
670+ conflicts = append (conflicts , fmt .Sprintf ("Installed version %s does not support operator-managed Gateway API. Install version %s or uninstall %s/%s to enable functionality." , subscription .Status .InstalledCSV , state .expectedGatewayAPIOperatorVersion , subscription .Namespace , subscription .Name ))
671+ case versionDiff == 0 :
672+ // Installed version is exactly as expected. Nothing to do.
673+ }
674+ } else {
675+ conflicts = append (conflicts , fmt .Sprintf ("Package %s from subscription %s/%s prevents enabling operator-managed Gateway API. Uninstall %s/%s to enable functionality." , subscription .Spec .Package , subscription .Namespace , subscription .Name , subscription .Namespace , subscription .Name ))
676+ }
677+ }
678+ if len (conflicts ) > 0 {
679+ degradedCondition .Status = configv1 .ConditionTrue
680+ degradedCondition .Reason = "GatewayAPIInstallConflict"
681+ degradedCondition .Message = strings .Join (conflicts , "\n " )
682+ } else if len (warnings ) > 0 {
683+ // Warnings are not enough to set degraded=true, but should still be included in the status message to warn
684+ // users to possible issues. Leave status=false and reason unset, but put warning messages in the message field.
685+ degradedCondition .Status = configv1 .ConditionFalse
686+ degradedCondition .Reason = "GatewayAPIInstallWarnings"
687+ degradedCondition .Message = strings .Join (warnings , "\n " )
688+ }
689+
690+ return degradedCondition
691+ }
692+
573693// computeOperatorUpgradeableCondition computes the operator's Upgradeable
574694// status condition.
575695func computeOperatorUpgradeableCondition (ingresses []operatorv1.IngressController ) configv1.ClusterOperatorStatusCondition {
@@ -834,7 +954,7 @@ func joinConditions(currCond, newCond configv1.ClusterOperatorStatusCondition) c
834954 case configv1 .ConditionTrue :
835955 if currCond .Status == configv1 .ConditionTrue {
836956 currCond .Reason += "And" + newCond .Reason
837- currCond .Message += newCond .Message
957+ currCond .Message += " \n " + newCond .Message
838958 } else {
839959 // Degraded=True status overrides other statuses.
840960 currCond .Status = newCond .Status
@@ -844,7 +964,7 @@ func joinConditions(currCond, newCond configv1.ClusterOperatorStatusCondition) c
844964 case configv1 .ConditionUnknown :
845965 if currCond .Status == configv1 .ConditionUnknown {
846966 currCond .Reason += "And" + newCond .Reason
847- currCond .Message += newCond .Message
967+ currCond .Message += " \n " + newCond .Message
848968 } else if currCond .Status != configv1 .ConditionTrue {
849969 // Degraded=Unknown overrides false and empty statuses.
850970 currCond .Status = newCond .Status
@@ -854,7 +974,7 @@ func joinConditions(currCond, newCond configv1.ClusterOperatorStatusCondition) c
854974 case configv1 .ConditionFalse :
855975 if currCond .Status == configv1 .ConditionFalse {
856976 currCond .Reason += "And" + newCond .Reason
857- currCond .Message += newCond .Message
977+ currCond .Message += " \n " + newCond .Message
858978 } else if currCond .Status == "" {
859979 // Degraded=False overrides empty status.
860980 currCond .Status = newCond .Status
@@ -864,3 +984,38 @@ func joinConditions(currCond, newCond configv1.ClusterOperatorStatusCondition) c
864984 }
865985 return currCond
866986}
987+
988+ // compareVersionNums compares two strings of format "<name>.v#.#.#". It returns a negative number if 'a' has a higher
989+ // version number, a positive number if 'b' has a higher version number, or 0 if the version numbers are identical. If
990+ // it is unable to parse either version number, or if the names from the two version strings differ, an error is
991+ // returned and the comparison should be considered invalid.
992+ func compareVersionNums (a , b string ) (int , error ) {
993+ var aName , bName string
994+ var aX , aY , aZ , bX , bY , bZ int
995+ aSplit := strings .Split (a , "." )
996+ if len (aSplit ) != 4 {
997+ return 0 , fmt .Errorf ("%q does not match expected format" , a )
998+ }
999+ aName = aSplit [0 ]
1000+ aX , _ = strconv .Atoi (aSplit [1 ][1 :]) // X has format "v%d", so [1:] cuts out the "v"
1001+ aY , _ = strconv .Atoi (aSplit [2 ])
1002+ aZ , _ = strconv .Atoi (aSplit [3 ])
1003+ bSplit := strings .Split (b , "." )
1004+ if len (bSplit ) != 4 {
1005+ return 0 , fmt .Errorf ("%q does not match expected format" , b )
1006+ }
1007+ bName = bSplit [0 ]
1008+ bX , _ = strconv .Atoi (bSplit [1 ][1 :]) // X has format "v%d", so [1:] cuts out the "v"
1009+ bY , _ = strconv .Atoi (bSplit [2 ])
1010+ bZ , _ = strconv .Atoi (bSplit [3 ])
1011+ if aName != bName {
1012+ return 0 , fmt .Errorf ("%q and %q are different packages. cannot compare version numbers" , a , b )
1013+ }
1014+ if aX != bX {
1015+ return bX - aX , nil
1016+ }
1017+ if aY != bY {
1018+ return bY - aY , nil
1019+ }
1020+ return bZ - aZ , nil
1021+ }
0 commit comments