Skip to content

Commit e2c66d3

Browse files
OCPBUGS-31550: Ensure SMCP and Gateway API subscription are reconciled upon deletion
This change ensures that the SMCP and Gateway API subscription get recreated if deleted. The OSSM operator installs the maistra.io CRDs. Need to create the SMCP watch after the OSSM operator is installed. Acknowledging @rfredette's input in #1115 (comment). Co-authored-by: Ryan Fredette <[email protected]>
1 parent 8be1749 commit e2c66d3

File tree

5 files changed

+160
-1
lines changed

5 files changed

+160
-1
lines changed

pkg/operator/controller/gatewayclass/controller.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@ package gatewayclass
22

33
import (
44
"context"
5+
"fmt"
6+
"sync"
57

68
logf "github.com/openshift/cluster-ingress-operator/pkg/log"
9+
operatorcontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller"
10+
11+
maistrav2 "github.com/maistra/istio-operator/pkg/apis/maistra/v2"
12+
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
713

814
"k8s.io/client-go/tools/record"
915

1016
gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1"
1117

18+
"k8s.io/apimachinery/pkg/types"
1219
utilerrors "k8s.io/apimachinery/pkg/util/errors"
1320

1421
"sigs.k8s.io/controller-runtime/pkg/cache"
@@ -35,6 +42,7 @@ const (
3542
)
3643

3744
var log = logf.Logger.WithName(controllerName)
45+
var gatewayClassController controller.Controller
3846

3947
// NewUnmanaged creates and returns a controller that watches gatewayclasses and
4048
// installs and configures Istio. This is an unmanaged controller, which means
@@ -61,6 +69,15 @@ func NewUnmanaged(mgr manager.Manager, config Config) (controller.Controller, er
6169
if err := c.Watch(source.Kind[client.Object](operatorCache, &gatewayapiv1.GatewayClass{}, &handler.EnqueueRequestForObject{}, isOurGatewayClass, notIstioGatewayClass)); err != nil {
6270
return nil, err
6371
}
72+
73+
isServiceMeshSubscription := predicate.NewPredicateFuncs(func(o client.Object) bool {
74+
return o.GetName() == operatorcontroller.ServiceMeshSubscriptionName().Name
75+
})
76+
if err = c.Watch(source.Kind[client.Object](operatorCache, &operatorsv1alpha1.Subscription{},
77+
enqueueRequestForDefaultGatewayClassController(config.OperandNamespace), isServiceMeshSubscription)); err != nil {
78+
return nil, err
79+
}
80+
gatewayClassController = c
6481
return c, nil
6582
}
6683

@@ -81,6 +98,23 @@ type reconciler struct {
8198
client client.Client
8299
cache cache.Cache
83100
recorder record.EventRecorder
101+
102+
startSMCPWatch sync.Once
103+
}
104+
105+
func enqueueRequestForDefaultGatewayClassController(namespace string) handler.EventHandler {
106+
return handler.EnqueueRequestsFromMapFunc(
107+
func(ctx context.Context, a client.Object) []reconcile.Request {
108+
return []reconcile.Request{
109+
{
110+
NamespacedName: types.NamespacedName{
111+
Namespace: namespace,
112+
Name: OpenShiftDefaultGatewayClassName,
113+
},
114+
},
115+
}
116+
},
117+
)
84118
}
85119

86120
// Reconcile expects request to refer to a GatewayClass and creates or
@@ -95,10 +129,23 @@ func (r *reconciler) Reconcile(ctx context.Context, request reconcile.Request) (
95129

96130
var errs []error
97131
if _, _, err := r.ensureServiceMeshOperatorSubscription(ctx); err != nil {
98-
errs = append(errs, err)
132+
errs = append(errs, fmt.Errorf("failed to ensure ServiceMeshOperatorSubscription: %w", err))
99133
}
100134
if _, _, err := r.ensureServiceMeshControlPlane(ctx, &gatewayclass); err != nil {
101135
errs = append(errs, err)
136+
} else {
137+
// The OSSM operator installs the maistra.io CRDs, Need to create the SMCP watch after the OSSM operator is installed.
138+
// Using sync.Once here, to start the watch for SMCP, which should run only once.
139+
r.startSMCPWatch.Do(func() {
140+
isOurSMCP := predicate.NewPredicateFuncs(func(o client.Object) bool {
141+
return o.GetName() == operatorcontroller.ServiceMeshControlPlaneName(r.config.OperandNamespace).Name
142+
})
143+
if err = gatewayClassController.Watch(source.Kind[client.Object](r.cache, &maistrav2.ServiceMeshControlPlane{}, enqueueRequestForDefaultGatewayClassController(r.config.OperandNamespace), isOurSMCP)); err != nil {
144+
log.Error(err, "failed to watch ServiceMeshControlPlane", "request", request)
145+
errs = append(errs, err)
146+
}
147+
})
102148
}
149+
103150
return reconcile.Result{}, utilerrors.NewAggregate(errs)
104151
}

pkg/operator/controller/names.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ const (
4747
// Remote worker label, used for node affinity of router deployment.
4848
// Router should not run on remote worker nodes
4949
RemoteWorkerLabel = "node.openshift.io/remote-worker"
50+
51+
// OpenshiftOperatorNamespace is the default namespace for
52+
// the openshift operator resources.
53+
OpenshiftOperatorNamespace = "openshift-operators"
5054
)
5155

5256
// IngressClusterOperatorName returns the namespaced name of the ClusterOperator

pkg/operator/operator.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ func New(config operatorconfig.Config, kubeConfig *rest.Config) (*Operator, erro
147147
operatorcontroller.DefaultOperandNamespace: {},
148148
operatorcontroller.DefaultCanaryNamespace: {},
149149
operatorcontroller.GlobalMachineSpecifiedConfigNamespace: {},
150+
operatorcontroller.OpenshiftOperatorNamespace: {},
150151
},
151152
},
152153
// Use a non-caching client everywhere. The default split client does not

test/e2e/gateway_api_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ func testGatewayAPIResources(t *testing.T) {
9999
// - the OSSM Istio operator is installed successfully and has status Running and Ready. e.g. istio-operator-9f5c88857-2xfrr -n openshift-operators
100100
// - Istiod is installed successfully and has status Running and Ready. e.g istiod-openshift-gateway-867bb8d5c7-4z6mp -n openshift-ingress
101101
// - the SMCP is created successfully (OSSM 2.x).
102+
// - deletes SMCP and subscription and tests if it gets recreated
102103
func testGatewayAPIIstioInstallation(t *testing.T) {
103104
t.Helper()
104105

@@ -118,6 +119,22 @@ func testGatewayAPIIstioInstallation(t *testing.T) {
118119
if err := assertSMCP(t); err != nil {
119120
t.Fatalf("failed to find expected SMCP: %v", err)
120121
}
122+
// delete existing SMCP to test it gets recreated
123+
if err := deleteExistingSMCP(t); err != nil {
124+
t.Fatalf("failed to delete existing SMCP: %v", err)
125+
}
126+
// check if SMCP gets recreated
127+
if err := assertSMCP(t); err != nil {
128+
t.Fatalf("failed to find expected SMCP: %v", err)
129+
}
130+
// delete existing Subscription to test it gets recreated
131+
if err := deleteExistingSubscription(t, openshiftOperatorsNamespace, expectedSubscriptionName); err != nil {
132+
t.Fatalf("failed to delete existing Subscription %s: %v", expectedSubscriptionName, err)
133+
}
134+
// checks if subscription gets recreated.
135+
if err := assertSubscription(t, openshiftOperatorsNamespace, expectedSubscriptionName); err != nil {
136+
t.Fatalf("failed to find expected Subscription %s: %v", expectedSubscriptionName, err)
137+
}
121138
}
122139

123140
// testGatewayAPIObjects tests that Gateway API objects can be created successfully.

test/e2e/util_gatewayapi_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,51 @@ func assertSubscription(t *testing.T, namespace, subName string) error {
289289
return err
290290
}
291291

292+
// deleteExistingSubscription deletes if the subscription of the given name exists and returns an error if not.
293+
func deleteExistingSubscription(t *testing.T, namespace, subName string) error {
294+
t.Helper()
295+
existingSubscription := &operatorsv1alpha1.Subscription{}
296+
newSubscription := &operatorsv1alpha1.Subscription{}
297+
nsName := types.NamespacedName{Namespace: namespace, Name: subName}
298+
299+
err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 30*time.Second, false, func(context context.Context) (bool, error) {
300+
if err := kclient.Get(context, nsName, existingSubscription); err != nil {
301+
t.Logf("failed to get Subscription %s: %v", nsName.Name, err)
302+
return false, nil
303+
}
304+
return true, nil
305+
})
306+
if err != nil {
307+
return fmt.Errorf("timed out getting subscription %s: %w", nsName.Name, err)
308+
}
309+
// deleting Subscription.
310+
err = kclient.Delete(context.Background(), existingSubscription)
311+
if err != nil {
312+
return err
313+
}
314+
err = wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 1*time.Minute, false, func(ctx context.Context) (bool, error) {
315+
if err := kclient.Get(ctx, nsName, newSubscription); err != nil {
316+
if kerrors.IsNotFound(err) {
317+
return true, nil
318+
}
319+
t.Logf("failed to get deleted subscription %s: %v", nsName.Name, err)
320+
return false, nil
321+
}
322+
// if new Subscription got recreated while the poll ensures the Subscription is deleted.
323+
if newSubscription != nil && newSubscription.UID != existingSubscription.UID {
324+
return true, nil
325+
}
326+
t.Logf("Subscription %s still exists", nsName.Name)
327+
return false, nil
328+
})
329+
if err != nil {
330+
return fmt.Errorf("timed out waiting for Subscription %s to be deleted: %v", nsName.Name, err)
331+
}
332+
t.Logf("deleted Subscription %s", nsName.Name)
333+
return nil
334+
335+
}
336+
292337
// assertOSSMOperator checks if the OSSM Istio operator gets successfully installed
293338
// and returns an error if not.
294339
func assertOSSMOperator(t *testing.T) error {
@@ -625,6 +670,51 @@ func assertSMCP(t *testing.T) error {
625670
return err
626671
}
627672

673+
// deleteExistingSMCP deletes if the SMCP exists and returns an error if not.
674+
func deleteExistingSMCP(t *testing.T) error {
675+
t.Helper()
676+
existingSMCP := &maistrav2.ServiceMeshControlPlane{}
677+
newSMCP := &maistrav2.ServiceMeshControlPlane{}
678+
nsName := types.NamespacedName{Namespace: operatorcontroller.DefaultOperandNamespace, Name: openshiftSMCPName}
679+
680+
err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 30*time.Second, false, func(context context.Context) (bool, error) {
681+
if err := kclient.Get(context, nsName, existingSMCP); err != nil {
682+
t.Logf("failed to get smcp %s: %v", nsName.Name, err)
683+
return false, nil
684+
}
685+
return true, nil
686+
})
687+
if err != nil {
688+
return fmt.Errorf("timed out getting smcp %s: %w", nsName.Name, err)
689+
}
690+
// deleting SMCP.
691+
err = kclient.Delete(context.Background(), existingSMCP)
692+
if err != nil {
693+
t.Errorf("failed to delete smcp %s: %v", nsName.Name, err)
694+
return err
695+
}
696+
err = wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 1*time.Minute, false, func(ctx context.Context) (bool, error) {
697+
if err := kclient.Get(ctx, nsName, newSMCP); err != nil {
698+
if kerrors.IsNotFound(err) {
699+
return true, nil
700+
}
701+
t.Logf("failed to get deleted SMCP %s: %v", nsName.Name, err)
702+
return false, nil
703+
}
704+
// if new SMCP got recreated while the poll ensures the SMCP is deleted.
705+
if newSMCP != nil && newSMCP.UID != existingSMCP.UID {
706+
return true, nil
707+
}
708+
t.Logf("smcp %s still exists", nsName.Name)
709+
return false, nil
710+
})
711+
if err != nil {
712+
return fmt.Errorf("timed out waiting for SMCP %s to be deleted: %v", nsName.Name, err)
713+
}
714+
t.Logf("deleted smcp %s", nsName.Name)
715+
return nil
716+
}
717+
628718
// assertDNSRecord checks to make sure a DNSRecord exists in a ready state,
629719
// and returns an error if not.
630720
func assertDNSRecord(t *testing.T, recordName types.NamespacedName) error {

0 commit comments

Comments
 (0)