Skip to content

Commit b966710

Browse files
Merge pull request #1268 from rfredette/ne-2066-conflicting-subs
NE-2066: Set degraded=true when OSSM 3 can't be installed
2 parents 2f21242 + 1b58225 commit b966710

File tree

6 files changed

+392
-13
lines changed

6 files changed

+392
-13
lines changed

pkg/operator/controller/gatewayapi/controller.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ var log = logf.Logger.WithName(controllerName)
4040
func New(mgr manager.Manager, config Config) (controller.Controller, error) {
4141
operatorCache := mgr.GetCache()
4242
reconciler := &reconciler{
43-
client: mgr.GetClient(),
44-
cache: operatorCache,
45-
config: config,
43+
client: mgr.GetClient(),
44+
cache: operatorCache,
45+
config: config,
46+
fieldIndexer: mgr.GetFieldIndexer(),
4647
}
4748
c, err := controller.New(controllerName, mgr, controller.Options{Reconciler: reconciler})
4849
if err != nil {
@@ -124,6 +125,7 @@ type reconciler struct {
124125
client client.Client
125126
cache cache.Cache
126127
recorder record.EventRecorder
128+
fieldIndexer client.FieldIndexer
127129
startControllers sync.Once
128130
}
129131

@@ -165,6 +167,20 @@ func (r *reconciler) Reconcile(ctx context.Context, request reconcile.Request) (
165167
}
166168

167169
r.startControllers.Do(func() {
170+
// Index gateway classes based on their spec.controllerName
171+
if err := r.fieldIndexer.IndexField(
172+
context.Background(),
173+
&gatewayapiv1.GatewayClass{},
174+
operatorcontroller.GatewayClassIndexFieldName,
175+
client.IndexerFunc(func(o client.Object) []string {
176+
gatewayclass, ok := o.(*gatewayapiv1.GatewayClass)
177+
if !ok {
178+
return []string{}
179+
}
180+
return []string{string(gatewayclass.Spec.ControllerName)}
181+
})); err != nil {
182+
log.Error(err, "failed to add field indexer")
183+
}
168184
for i := range r.config.DependentControllers {
169185
c := &r.config.DependentControllers[i]
170186
go func() {

pkg/operator/controller/gatewayapi/controller_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ func Test_Reconcile(t *testing.T) {
283283
OperatorLifecycleManagerEnabled: tc.olmEnabled,
284284
DependentControllers: []controller.Controller{ctrl},
285285
},
286+
fieldIndexer: FakeIndexer{},
286287
}
287288
req := reconcile.Request{
288289
NamespacedName: types.NamespacedName{
@@ -360,6 +361,7 @@ func TestReconcileOnlyStartsControllerOnce(t *testing.T) {
360361
OperatorLifecycleManagerEnabled: true,
361362
DependentControllers: []controller.Controller{ctrl},
362363
},
364+
fieldIndexer: FakeIndexer{},
363365
}
364366
req := reconcile.Request{NamespacedName: types.NamespacedName{Name: "cluster"}}
365367

@@ -389,3 +391,9 @@ func TestReconcileOnlyStartsControllerOnce(t *testing.T) {
389391
}
390392
assert.True(t, ctrl.Started, "fake controller should have been started")
391393
}
394+
395+
type FakeIndexer struct{}
396+
397+
func (indexer FakeIndexer) IndexField(ctx context.Context, obj client.Object, field string, extractValue client.IndexerFunc) error {
398+
return nil
399+
}

pkg/operator/controller/names.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ const (
7676
// IstioRevLabelKey is the key for the gateway label that Istio checks
7777
// for to determine whether it should reconcile that gateway.
7878
IstioRevLabelKey = "istio.io/rev"
79+
80+
GatewayClassIndexFieldName = "gatewayclassController"
7981
)
8082

8183
// IngressClusterOperatorName returns the namespaced name of the ClusterOperator

pkg/operator/controller/status/controller.go

Lines changed: 163 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"sort"
8+
"strconv"
89
"strings"
910

1011
"github.com/google/go-cmp/cmp"
@@ -72,6 +73,14 @@ var (
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.
8493
func 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 {
192223
type 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.
575695
func 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

Comments
 (0)