-
Notifications
You must be signed in to change notification settings - Fork 218
NE-1969: Set Degraded=True if unmanaged Gateway API CRDs exist #1205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ package gatewayapi | |
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "sync" | ||
|
|
||
| logf "github.com/openshift/cluster-ingress-operator/pkg/log" | ||
|
|
@@ -14,6 +15,7 @@ import ( | |
| apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" | ||
| "k8s.io/apimachinery/pkg/types" | ||
|
|
||
| "sigs.k8s.io/controller-runtime/pkg/cache" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| "sigs.k8s.io/controller-runtime/pkg/controller" | ||
| "sigs.k8s.io/controller-runtime/pkg/handler" | ||
|
|
@@ -25,7 +27,10 @@ import ( | |
| ) | ||
|
|
||
| const ( | ||
| controllerName = "gatewayapi_controller" | ||
| controllerName = "gatewayapi_controller" | ||
| experimentalGatewayAPIGroupName = "gateway.networking.x-k8s.io" | ||
| gatewayAPICRDIndexFieldName = "gatewayAPICRD" | ||
| unmanagedGatewayAPICRDIndexFieldValue = "unmanaged" | ||
| ) | ||
|
|
||
| var log = logf.Logger.WithName(controllerName) | ||
|
|
@@ -36,6 +41,7 @@ func New(mgr manager.Manager, config Config) (controller.Controller, error) { | |
| operatorCache := mgr.GetCache() | ||
| reconciler := &reconciler{ | ||
| client: mgr.GetClient(), | ||
| cache: operatorCache, | ||
| config: config, | ||
| } | ||
| c, err := controller.New(controllerName, mgr, controller.Options{Reconciler: reconciler}) | ||
|
|
@@ -60,14 +66,34 @@ func New(mgr manager.Manager, config Config) (controller.Controller, error) { | |
| }} | ||
| } | ||
|
|
||
| // watch for CRDs | ||
| crdPredicate := predicate.NewPredicateFuncs(func(o client.Object) bool { | ||
| return o.(*apiextensionsv1.CustomResourceDefinition).Spec.Group == gatewayapiv1.GroupName | ||
| }) | ||
| isGatewayAPICRD := func(o client.Object) bool { | ||
| crd := o.(*apiextensionsv1.CustomResourceDefinition) | ||
| return crd.Spec.Group == gatewayapiv1.GroupName || crd.Spec.Group == experimentalGatewayAPIGroupName | ||
| } | ||
| crdPredicate := predicate.NewPredicateFuncs(isGatewayAPICRD) | ||
|
|
||
| // watch for CRDs | ||
| if err := c.Watch(source.Kind[client.Object](operatorCache, &apiextensionsv1.CustomResourceDefinition{}, handler.EnqueueRequestsFromMapFunc(toFeatureGate), crdPredicate)); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // Index unmanaged Gateway API CRDs to enable efficient filtering | ||
| // during list operations. | ||
| if err := mgr.GetFieldIndexer().IndexField( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you create two indexers, one for managed and one for unmanaged CRDs? See https://github.com/openshift/cluster-ingress-operator/pull/1205/files?diff=split&w=1#r2019309675
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What would be the purpose of having two indexers?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I see your comment on Wouldn't it make more sense to do this? client.IndexerFunc(func(o client.Object) []string {
if isGatewayAPICRD(o) {
return []string{"managed"}
}
return []string{"unmanaged"}
})and then r.cache.List(ctx, gatewayAPICRDs, client.MatchingFields{crdAPIGroupIndexFieldName: "managed"});or r.cache.List(ctx, gatewayAPICRDs, client.MatchingFields{crdAPIGroupIndexFieldName: "unmanaged"});
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't quite know why we need this indexer, which picks up managed and unmanaged CRDs together, but there is code around https://github.com/openshift/cluster-ingress-operator/pull/1205/files?diff=split&w=1#r2019309675 that gets all the CRDs from this index and then iterates through to get the unmanaged ones. One indexer for unmanaged CRDs should be enough.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I guess my last comment doesn't make sense. The index is used in order to get all Gateway API CRDs, be they managed or be they unmanaged, and then the loop in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that 2 indexes do not make sense as "managed" index would be of no use in this case.
This may be another approach to the problem indeed. I was thinking more of the field name which matches the CRD field
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
My understanding from our call today is that we agreed on this approach: using one index, which has only CRDs that are both in any of the API groups that belong to Gateway API and not managed by cluster-ingress-operator.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed the index to have a field for all unmanaged gateway API CRDs. |
||
| context.Background(), | ||
| &apiextensionsv1.CustomResourceDefinition{}, | ||
| gatewayAPICRDIndexFieldName, | ||
| client.IndexerFunc(func(o client.Object) []string { | ||
| if isGatewayAPICRD(o) { | ||
| if _, found := managedCRDMap[o.GetName()]; !found { | ||
| return []string{unmanagedGatewayAPICRDIndexFieldValue} | ||
| } | ||
| } | ||
| return []string{} | ||
| })); err != nil { | ||
| return nil, fmt.Errorf("failed to create index for custom resource definitions: %w", err) | ||
| } | ||
|
|
||
| return c, nil | ||
| } | ||
|
|
||
|
|
@@ -90,6 +116,7 @@ type reconciler struct { | |
| config Config | ||
|
|
||
| client client.Client | ||
| cache cache.Cache | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it is still used in |
||
| recorder record.EventRecorder | ||
| startControllers sync.Once | ||
| } | ||
|
|
@@ -111,6 +138,12 @@ func (r *reconciler) Reconcile(ctx context.Context, request reconcile.Request) ( | |
| return reconcile.Result{}, err | ||
| } | ||
|
|
||
| if crdNames, err := r.listUnmanagedGatewayAPICRDs(ctx); err != nil { | ||
| return reconcile.Result{}, fmt.Errorf("failed to list unmanaged gateway CRDs: %w", err) | ||
| } else if err = r.setUnmanagedGatewayAPICRDNamesStatus(ctx, crdNames); err != nil { | ||
| return reconcile.Result{}, fmt.Errorf("failed to update the ingress cluster operator status: %w", err) | ||
alebedev87 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| if !r.config.GatewayAPIControllerEnabled { | ||
| return reconcile.Result{}, nil | ||
| } | ||
|
|
||
alebedev87 marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ package gatewayapi | |
|
|
||
| import ( | ||
| "context" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
|
|
||
|
|
@@ -17,6 +18,7 @@ import ( | |
| "k8s.io/apimachinery/pkg/runtime" | ||
| "k8s.io/apimachinery/pkg/types" | ||
|
|
||
| "sigs.k8s.io/controller-runtime/pkg/cache/informertest" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| "sigs.k8s.io/controller-runtime/pkg/client/fake" | ||
| "sigs.k8s.io/controller-runtime/pkg/controller" | ||
|
|
@@ -36,28 +38,57 @@ func Test_Reconcile(t *testing.T) { | |
| ObjectMeta: metav1.ObjectMeta{Name: name}, | ||
| } | ||
| } | ||
| co := func(name string) *configv1.ClusterOperator { | ||
| return &configv1.ClusterOperator{ | ||
| ObjectMeta: metav1.ObjectMeta{Name: name}, | ||
| } | ||
| } | ||
| coWithExtension := func(name, extension string) *configv1.ClusterOperator { | ||
| co := co(name) | ||
| co.Status = configv1.ClusterOperatorStatus{ | ||
| Extension: runtime.RawExtension{ | ||
| Raw: []byte(extension), | ||
| }, | ||
| } | ||
| return co | ||
| } | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| gatewayAPIEnabled bool | ||
| gatewayAPIControllerEnabled bool | ||
| existingObjects []runtime.Object | ||
| expectCreate []client.Object | ||
| expectUpdate []client.Object | ||
| expectDelete []client.Object | ||
| expectStartCtrl bool | ||
| // existingStatusSubresource contains the original version of objects | ||
| // whose status will updated by Reconcile function. | ||
| // This field is similar to `existingObjects` but is specifically used | ||
| // for objects where status updates are performed using `Status().Update()` call. | ||
| existingStatusSubresource []client.Object | ||
| expectCreate []client.Object | ||
| expectUpdate []client.Object | ||
| expectDelete []client.Object | ||
| // expectStatusUpdate contains the updated versions of objects | ||
| // whose status is expected to be updated by the test. | ||
| expectStatusUpdate []client.Object | ||
| expectStartCtrl bool | ||
| }{ | ||
| { | ||
| name: "gateway API disabled", | ||
| gatewayAPIEnabled: false, | ||
| expectCreate: []client.Object{}, | ||
| expectUpdate: []client.Object{}, | ||
| expectDelete: []client.Object{}, | ||
| expectStartCtrl: false, | ||
| existingObjects: []runtime.Object{ | ||
| co("ingress"), | ||
| }, | ||
| expectCreate: []client.Object{}, | ||
| expectUpdate: []client.Object{}, | ||
| expectDelete: []client.Object{}, | ||
| expectStartCtrl: false, | ||
| }, | ||
| { | ||
| name: "gateway API enabled", | ||
| gatewayAPIEnabled: true, | ||
| gatewayAPIControllerEnabled: true, | ||
| existingObjects: []runtime.Object{ | ||
| co("ingress"), | ||
| }, | ||
| expectCreate: []client.Object{ | ||
| crd("gatewayclasses.gateway.networking.k8s.io"), | ||
| crd("gateways.gateway.networking.k8s.io"), | ||
|
|
@@ -75,6 +106,9 @@ func Test_Reconcile(t *testing.T) { | |
| name: "gateway API enabled, gateway API controller disabled", | ||
| gatewayAPIEnabled: true, | ||
| gatewayAPIControllerEnabled: false, | ||
| existingObjects: []runtime.Object{ | ||
| co("ingress"), | ||
| }, | ||
| expectCreate: []client.Object{ | ||
| crd("gatewayclasses.gateway.networking.k8s.io"), | ||
| crd("gateways.gateway.networking.k8s.io"), | ||
|
|
@@ -88,6 +122,87 @@ func Test_Reconcile(t *testing.T) { | |
| expectDelete: []client.Object{}, | ||
| expectStartCtrl: false, | ||
| }, | ||
| { | ||
| name: "unmanaged gateway API CRDs created", | ||
| gatewayAPIEnabled: true, | ||
| gatewayAPIControllerEnabled: true, | ||
| existingObjects: []runtime.Object{ | ||
| co("ingress"), | ||
| crd("listenersets.gateway.networking.x-k8s.io"), | ||
| crd("backendtrafficpolicies.gateway.networking.x-k8s.io"), | ||
| }, | ||
| existingStatusSubresource: []client.Object{ | ||
| co("ingress"), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a comment about why we need an ingress co in both existingObjects and existingStatusSubresource? Do they represent the same ingress co?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, they do. I have a comment for |
||
| }, | ||
| expectCreate: []client.Object{ | ||
| crd("gatewayclasses.gateway.networking.k8s.io"), | ||
| crd("gateways.gateway.networking.k8s.io"), | ||
| crd("grpcroutes.gateway.networking.k8s.io"), | ||
| crd("httproutes.gateway.networking.k8s.io"), | ||
| crd("referencegrants.gateway.networking.k8s.io"), | ||
| clusterRole("system:openshift:gateway-api:aggregate-to-admin"), | ||
| clusterRole("system:openshift:gateway-api:aggregate-to-view"), | ||
| }, | ||
| expectUpdate: []client.Object{}, | ||
| expectDelete: []client.Object{}, | ||
| expectStatusUpdate: []client.Object{ | ||
| coWithExtension("ingress", `{"unmanagedGatewayAPICRDNames":"backendtrafficpolicies.gateway.networking.x-k8s.io,listenersets.gateway.networking.x-k8s.io"}`), | ||
| }, | ||
| expectStartCtrl: true, | ||
| }, | ||
| { | ||
| name: "unmanaged gateway API CRDs removed", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what's happening here. Is this simulating the removal of unmanaged CRDs?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. We have the ingress cluster operator with |
||
| gatewayAPIEnabled: true, | ||
| gatewayAPIControllerEnabled: true, | ||
| existingObjects: []runtime.Object{ | ||
| coWithExtension("ingress", `{"unmanagedGatewayAPICRDNames":"listenersets.gateway.networking.x-k8s.io"}`), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding something like
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keeping unexpected fields in So the question is - do we want to allow unsolicited status extension data in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @alebedev87 can you follow this up later. I understand we don't need to account for other uses of extension now, but we should be prepared for the future. Or at least post a warning that more work needs to be done to share the extension.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Here is what the description of the extension field says:
So, I prefer to answer my own question with "no" because the cluster operator extension should not be updated by any third party (note that I covered the sharing of the extension between different controllers of the operator). And if it is, we should know about in a form of a bug (which we will have rights to challenge). |
||
| }, | ||
| existingStatusSubresource: []client.Object{ | ||
| co("ingress"), | ||
| }, | ||
| expectCreate: []client.Object{ | ||
| crd("gatewayclasses.gateway.networking.k8s.io"), | ||
| crd("gateways.gateway.networking.k8s.io"), | ||
| crd("grpcroutes.gateway.networking.k8s.io"), | ||
| crd("httproutes.gateway.networking.k8s.io"), | ||
| crd("referencegrants.gateway.networking.k8s.io"), | ||
| clusterRole("system:openshift:gateway-api:aggregate-to-admin"), | ||
| clusterRole("system:openshift:gateway-api:aggregate-to-view"), | ||
| }, | ||
| expectUpdate: []client.Object{}, | ||
| expectDelete: []client.Object{}, | ||
| expectStatusUpdate: []client.Object{ | ||
| coWithExtension("ingress", `{}`), | ||
| }, | ||
| expectStartCtrl: true, | ||
| }, | ||
| { | ||
| name: "third party CRDs", | ||
| gatewayAPIEnabled: true, | ||
| gatewayAPIControllerEnabled: true, | ||
| existingObjects: []runtime.Object{ | ||
| co("ingress"), | ||
| crd("thirdpartycrd1.openshift.io"), | ||
| crd("thirdpartycrd2.openshift.io"), | ||
| }, | ||
| existingStatusSubresource: []client.Object{ | ||
| co("ingress"), | ||
| }, | ||
| expectCreate: []client.Object{ | ||
| crd("gatewayclasses.gateway.networking.k8s.io"), | ||
| crd("gateways.gateway.networking.k8s.io"), | ||
| crd("grpcroutes.gateway.networking.k8s.io"), | ||
| crd("httproutes.gateway.networking.k8s.io"), | ||
| crd("referencegrants.gateway.networking.k8s.io"), | ||
| clusterRole("system:openshift:gateway-api:aggregate-to-admin"), | ||
| clusterRole("system:openshift:gateway-api:aggregate-to-view"), | ||
| }, | ||
| expectUpdate: []client.Object{}, | ||
| expectDelete: []client.Object{}, | ||
| // Third party CRDs have no impact on cluster operator status. | ||
| expectStatusUpdate: []client.Object{}, | ||
| expectStartCtrl: true, | ||
| }, | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add another test for experimental gateway CRDs.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a test case called
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That ListenerSet CRD is already existing in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is tested in the e2e test I added, because it involves 2 different controllers (gatewayapi and status). |
||
|
|
||
| scheme := runtime.NewScheme() | ||
|
|
@@ -100,11 +215,31 @@ func Test_Reconcile(t *testing.T) { | |
| fakeClient := fake.NewClientBuilder(). | ||
| WithScheme(scheme). | ||
| WithRuntimeObjects(tc.existingObjects...). | ||
| WithStatusSubresource(tc.existingStatusSubresource...). | ||
| WithIndex(&apiextensionsv1.CustomResourceDefinition{}, "gatewayAPICRD", client.IndexerFunc(func(o client.Object) []string { | ||
| // Assume that all experimental CRDs are unmanaged. | ||
| if strings.Contains(o.GetName(), "gateway.networking.x-k8s.io") { | ||
| return []string{"unmanaged"} | ||
| } | ||
| return []string{} | ||
| })). | ||
| Build() | ||
| cl := &testutil.FakeClientRecorder{fakeClient, t, []client.Object{}, []client.Object{}, []client.Object{}} | ||
| cl := &testutil.FakeClientRecorder{ | ||
| Client: fakeClient, | ||
| T: t, | ||
| Added: []client.Object{}, | ||
| Updated: []client.Object{}, | ||
| Deleted: []client.Object{}, | ||
| StatusWriter: &testutil.FakeStatusWriter{ | ||
| StatusWriter: fakeClient.Status(), | ||
| }, | ||
| } | ||
| ctrl := &testutil.FakeController{t, false, nil} | ||
| informer := informertest.FakeInformers{Scheme: scheme} | ||
| cache := &testutil.FakeCache{Informers: &informer, Reader: fakeClient} | ||
| reconciler := &reconciler{ | ||
| client: cl, | ||
| cache: cache, | ||
| config: Config{ | ||
| GatewayAPIEnabled: tc.gatewayAPIEnabled, | ||
| GatewayAPIControllerEnabled: tc.gatewayAPIControllerEnabled, | ||
|
|
@@ -144,6 +279,9 @@ func Test_Reconcile(t *testing.T) { | |
| if diff := cmp.Diff(tc.expectDelete, cl.Deleted, cmpOpts...); diff != "" { | ||
| t.Fatalf("found diff between expected and actual deletes: %s", diff) | ||
| } | ||
| if diff := cmp.Diff(tc.expectStatusUpdate, cl.StatusWriter.Updated, cmpOpts...); diff != "" { | ||
| t.Fatalf("found diff between expected and actual status updates: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
@@ -153,11 +291,30 @@ func TestReconcileOnlyStartsControllerOnce(t *testing.T) { | |
| configv1.Install(scheme) | ||
| apiextensionsv1.AddToScheme(scheme) | ||
| rbacv1.AddToScheme(scheme) | ||
| fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects().Build() | ||
| cl := &testutil.FakeClientRecorder{fakeClient, t, []client.Object{}, []client.Object{}, []client.Object{}} | ||
| fakeClient := fake.NewClientBuilder(). | ||
| WithScheme(scheme). | ||
| WithRuntimeObjects( | ||
| &configv1.ClusterOperator{ | ||
| ObjectMeta: metav1.ObjectMeta{Name: "ingress"}, | ||
| }). | ||
| WithIndex(&apiextensionsv1.CustomResourceDefinition{}, "gatewayAPICRD", client.IndexerFunc(func(o client.Object) []string { | ||
| // Assume that there are no unmanaged CRDs. | ||
| return []string{} | ||
| })). | ||
| Build() | ||
| cl := &testutil.FakeClientRecorder{ | ||
| Client: fakeClient, | ||
| T: t, | ||
| Added: []client.Object{}, | ||
| Updated: []client.Object{}, | ||
| Deleted: []client.Object{}, | ||
| } | ||
| ctrl := &testutil.FakeController{t, false, make(chan struct{})} | ||
| informer := informertest.FakeInformers{Scheme: scheme} | ||
| cache := &testutil.FakeCache{Informers: &informer, Reader: fakeClient} | ||
| reconciler := &reconciler{ | ||
| client: cl, | ||
| cache: cache, | ||
| config: Config{ | ||
| GatewayAPIEnabled: true, | ||
| GatewayAPIControllerEnabled: true, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: it would be nice to define this in names.go so it could be used by other packages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently it's only used in this package. In the unit tests, I deliberately abstain from using the same constants as in the code to avoid duplicating errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It always seems wrong to me when we define a name that doesn't belong to the operator in
names.go. Maybe we could importsigs.k8s.io/gateway-api/apisx/v1alpha1and then usegatewayapixv1alpha1.GroupName? Alternatively, we could put something inmanifests.go.