Skip to content

Commit 5c489b5

Browse files
committed
[project/gateway2]: Adds GatewayClass SupportedVersion Condition
Previously, the gateway2 controller would unconditionally set the GatewayClass Accepted condition to `True`. This PR adds a watch for the Gateway API CRDs that triggers GatewayClass reconciliation. The `ReconcileGatewayClasses()` method was updated to set GatewayClass status conditionas based on the Gateway API spec. Signed-off-by: Daneyon Hansen <[email protected]>
1 parent a1dc990 commit 5c489b5

File tree

14 files changed

+469
-19
lines changed

14 files changed

+469
-19
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
changelog:
2+
- type: NEW_FEATURE
3+
issueLink: https://github.com/solo-io/gloo/issues/10092
4+
resolvesIssue: true
5+
description: >-
6+
Adds support for the GatewayClass `SupportedVersion` status condition and updates the ReconcileGatewayClasses()
7+
method to manage status conditions according to the Gateway API specification. The gateway controller will now
8+
reconcile supported Gateway API CRDs and set the `SupportedVersion` status condition based on the value of the
9+
`gateway.networking.k8s.io/bundle-version` annotation.

install/helm/gloo/templates/44-rbac.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ rules:
2929
resources:
3030
- endpointslices
3131
verbs: ["get", "list", "watch"]
32+
- apiGroups:
33+
- "apiextensions.k8s.io"
34+
resources:
35+
- customresourcedefinitions
36+
verbs: ["get", "list", "watch"]
3237
- apiGroups:
3338
- "gateway.solo.io"
3439
resources:

pkg/schemes/scheme.go

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
ratelimitv1alpha1 "github.com/solo-io/solo-apis/pkg/api/ratelimit.solo.io/v1alpha1"
1010
appsv1 "k8s.io/api/apps/v1"
1111
corev1 "k8s.io/api/core/v1"
12+
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1213
"k8s.io/apimachinery/pkg/runtime"
1314
apiv1 "sigs.k8s.io/gateway-api/apis/v1"
1415
apiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
@@ -25,6 +26,9 @@ var SchemeBuilder = runtime.SchemeBuilder{
2526
corev1.AddToScheme,
2627
appsv1.AddToScheme,
2728

29+
// Kubernetes apiextensions resources
30+
apiextv1.AddToScheme,
31+
2832
// Solo Kubernetes Gateway API resources
2933
sologatewayv1alpha1.AddToScheme,
3034

projects/gateway2/controller/controller.go

+119-17
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ package controller
33
import (
44
"context"
55
"fmt"
6+
"strings"
67

78
corev1 "k8s.io/api/core/v1"
9+
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
10+
"k8s.io/apimachinery/pkg/api/errors"
811
"k8s.io/apimachinery/pkg/api/meta"
912
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1013
"k8s.io/apimachinery/pkg/fields"
@@ -21,9 +24,11 @@ import (
2124
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2225
apiv1 "sigs.k8s.io/gateway-api/apis/v1"
2326
apiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
27+
"sigs.k8s.io/gateway-api/pkg/consts"
2428

2529
sologatewayv1 "github.com/solo-io/gloo/projects/gateway/pkg/api/v1/kube/apis/gateway.solo.io/v1"
2630
"github.com/solo-io/gloo/projects/gateway2/api/v1alpha1"
31+
"github.com/solo-io/gloo/projects/gateway2/crds"
2732
"github.com/solo-io/gloo/projects/gateway2/deployer"
2833
"github.com/solo-io/gloo/projects/gateway2/extensions"
2934
"github.com/solo-io/gloo/projects/gateway2/query"
@@ -62,13 +67,15 @@ func NewBaseGatewayController(ctx context.Context, cfg GatewayConfig) error {
6267
controllerBuilder := &controllerBuilder{
6368
cfg: cfg,
6469
reconciler: &controllerReconciler{
65-
cli: cfg.Mgr.GetClient(),
66-
scheme: cfg.Mgr.GetScheme(),
67-
kick: cfg.Kick,
70+
controllerName: cfg.ControllerName,
71+
cli: cfg.Mgr.GetClient(),
72+
scheme: cfg.Mgr.GetScheme(),
73+
kick: cfg.Kick,
6874
},
6975
}
7076

7177
return run(ctx,
78+
controllerBuilder.watchCRDs,
7279
controllerBuilder.watchGwClass,
7380
controllerBuilder.watchGw,
7481
controllerBuilder.watchHttpRoute,
@@ -244,6 +251,26 @@ func shouldIgnoreStatusChild(gvk schema.GroupVersionKind) bool {
244251
return gvk.Kind == "Deployment"
245252
}
246253

254+
// watchCRDs sets up a controller to watch for changes to specific Gateway API CRDs and triggers
255+
// GatewayClass reconciliation if generation or annotations change.
256+
func (c *controllerBuilder) watchCRDs(ctx context.Context) error {
257+
return ctrl.NewControllerManagedBy(c.cfg.Mgr).
258+
For(&apiextv1.CustomResourceDefinition{}).
259+
WithEventFilter(predicate.NewPredicateFuncs(func(object client.Object) bool {
260+
// We care about changes to the specific CRDs
261+
crd, ok := object.(*apiextv1.CustomResourceDefinition)
262+
if !ok {
263+
return false
264+
}
265+
return crds.IsSupported(crd.Name)
266+
})).
267+
WithEventFilter(predicate.Or(
268+
predicate.AnnotationChangedPredicate{},
269+
predicate.GenerationChangedPredicate{},
270+
)).
271+
Complete(reconcile.Func(c.reconciler.ReconcileCRDs))
272+
}
273+
247274
func (c *controllerBuilder) watchGwClass(_ context.Context) error {
248275
return ctrl.NewControllerManagedBy(c.cfg.Mgr).
249276
WithEventFilter(predicate.GenerationChangedPredicate{}).
@@ -330,9 +357,10 @@ func (c *controllerBuilder) watchDirectResponses(_ context.Context) error {
330357
}
331358

332359
type controllerReconciler struct {
333-
cli client.Client
334-
scheme *runtime.Scheme
335-
kick func(ctx context.Context)
360+
controllerName string
361+
cli client.Client
362+
scheme *runtime.Scheme
363+
kick func(ctx context.Context)
336364
}
337365

338366
func (r *controllerReconciler) ReconcileHttpListenerOptions(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
@@ -408,8 +436,39 @@ func (r *controllerReconciler) ReconcileReferenceGrants(ctx context.Context, req
408436
return ctrl.Result{}, nil
409437
}
410438

439+
func (r *controllerReconciler) ReconcileCRDs(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
440+
log := log.FromContext(ctx).WithValues("CustomResourceDefinition", req.Name)
441+
442+
log.Info("reconciling CustomResourceDefinition")
443+
444+
var gatewayClassList apiv1.GatewayClassList
445+
if err := r.cli.List(ctx, &gatewayClassList); err != nil {
446+
log.Error(err, "Failed to list GatewayClasses")
447+
return ctrl.Result{}, err
448+
}
449+
450+
for _, gatewayClass := range gatewayClassList.Items {
451+
if gatewayClass.Spec.ControllerName == apiv1.GatewayController(r.controllerName) {
452+
req := ctrl.Request{
453+
NamespacedName: client.ObjectKey{
454+
Name: gatewayClass.Name,
455+
},
456+
}
457+
458+
if _, err := r.ReconcileGatewayClasses(ctx, req); err != nil {
459+
log.Error(err, "Failed to reconcile GatewayClass", "name", gatewayClass.Name)
460+
return ctrl.Result{}, err
461+
}
462+
}
463+
}
464+
465+
log.Info("Reconciled CustomResourceDefinition")
466+
467+
return ctrl.Result{}, nil
468+
}
469+
411470
func (r *controllerReconciler) ReconcileGatewayClasses(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
412-
log := log.FromContext(ctx).WithValues("gwclass", req.NamespacedName)
471+
log := log.FromContext(ctx).WithValues("GatewayClass", req.Name)
413472

414473
gwclass := &apiv1.GatewayClass{}
415474
if err := r.cli.Get(ctx, req.NamespacedName, gwclass); err != nil {
@@ -420,31 +479,74 @@ func (r *controllerReconciler) ReconcileGatewayClasses(ctx context.Context, req
420479
return ctrl.Result{}, client.IgnoreNotFound(err)
421480
}
422481

423-
log.Info("reconciling gateway class")
482+
log.Info("Reconciling GatewayClass")
424483

425-
// mark it as accepted:
484+
// Initialize the status conditions. No need to set LastTransitionTime since it's handled by SetStatusCondition.
485+
msg := "Gateway API CRDs are a supported version"
426486
acceptedCondition := metav1.Condition{
427487
Type: string(apiv1.GatewayClassConditionStatusAccepted),
428488
Status: metav1.ConditionTrue,
429489
Reason: string(apiv1.GatewayClassReasonAccepted),
490+
Message: msg,
430491
ObservedGeneration: gwclass.Generation,
431-
// no need to set LastTransitionTime, it will be set automatically by SetStatusCondition
432492
}
433-
meta.SetStatusCondition(&gwclass.Status.Conditions, acceptedCondition)
434-
435-
// TODO: This should actually check the version of the CRDs in the cluster to be 100% sure
436-
supportedVersionCondition := metav1.Condition{
493+
supportedCondition := metav1.Condition{
437494
Type: string(apiv1.GatewayClassConditionStatusSupportedVersion),
438495
Status: metav1.ConditionTrue,
439-
ObservedGeneration: gwclass.Generation,
440496
Reason: string(apiv1.GatewayClassReasonSupportedVersion),
497+
Message: msg,
498+
ObservedGeneration: gwclass.Generation,
499+
}
500+
501+
// Check CRD versions
502+
supported, err := r.checkCRDVersions(ctx)
503+
if err != nil {
504+
log.Error(err, "Failed to check CRD versions")
505+
return ctrl.Result{}, err
506+
}
507+
508+
if !supported {
509+
// Update the values of status conditions
510+
msg = fmt.Sprintf("Unsupported Gateway API CRDs detected. Supported versions are: %s",
511+
strings.Join(wellknown.SupportedVersions, ", "))
512+
acceptedCondition.Status = metav1.ConditionFalse
513+
acceptedCondition.Reason = string(apiv1.GatewayClassReasonUnsupportedVersion)
514+
acceptedCondition.Message = msg
515+
supportedCondition.Status = metav1.ConditionFalse
516+
supportedCondition.Reason = string(apiv1.GatewayClassReasonUnsupportedVersion)
517+
supportedCondition.Message = msg
441518
}
442-
meta.SetStatusCondition(&gwclass.Status.Conditions, supportedVersionCondition)
519+
520+
// Set the status conditions
521+
meta.SetStatusCondition(&gwclass.Status.Conditions, acceptedCondition)
522+
meta.SetStatusCondition(&gwclass.Status.Conditions, supportedCondition)
443523

444524
if err := r.cli.Status().Update(ctx, gwclass); err != nil {
525+
log.Error(err, "Failed to update GatewayClass status")
445526
return ctrl.Result{}, err
446527
}
447-
log.Info("updated gateway class status")
528+
529+
log.Info("Reconciled GatewayClass")
448530

449531
return ctrl.Result{}, nil
450532
}
533+
534+
// checkCRDVersions checks that the "gateway.networking.k8s.io/bundle-version" annotation key is set
535+
// to a supported version for each required Gateway API CRD.
536+
func (r *controllerReconciler) checkCRDVersions(ctx context.Context) (bool, error) {
537+
for _, crdName := range crds.Required {
538+
crd := &apiextv1.CustomResourceDefinition{}
539+
if err := r.cli.Get(ctx, client.ObjectKey{Name: crdName}, crd); err != nil {
540+
if errors.IsNotFound(err) {
541+
return false, nil
542+
}
543+
return false, err
544+
}
545+
546+
bundleVersion, exists := crd.Annotations[consts.BundleVersionAnnotation]
547+
if !exists || !crds.IsSupportedVersion(bundleVersion) {
548+
return false, nil
549+
}
550+
}
551+
return true, nil
552+
}

projects/gateway2/crds/crds.go

+32
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,39 @@ package crds
22

33
import (
44
_ "embed"
5+
6+
"k8s.io/apimachinery/pkg/util/sets"
7+
8+
"github.com/solo-io/gloo/projects/gateway2/wellknown"
9+
)
10+
11+
const (
12+
// GatewayClass is the name of the GatewayClass CRD.
13+
GatewayClass = "gatewayclasses.gateway.networking.k8s.io"
14+
// Gateway is the name of the Gateway CRD.
15+
Gateway = "gateways.gateway.networking.k8s.io"
16+
// HTTPRoute is the name of the HTTPRoute CRD.
17+
HTTPRoute = "httproutes.gateway.networking.k8s.io"
18+
// ReferenceGrant is the name of the ReferenceGrant CRD.
19+
ReferenceGrant = "referencegrants.gateway.networking.k8s.io"
520
)
621

722
//go:embed gateway-crds.yaml
823
var GatewayCrds []byte
24+
25+
// Required is a list of required Gateway API CRDs.
26+
var Required = []string{GatewayClass, Gateway, HTTPRoute, ReferenceGrant}
27+
28+
// IsSupportedVersion checks if the CRD version is recognized and supported.
29+
func IsSupportedVersion(version string) bool {
30+
supportedVersions := sets.NewString(wellknown.SupportedVersions...)
31+
return supportedVersions.Has(version)
32+
}
33+
34+
// IsSupported checks if the CRD is supported based on the provided name.
35+
func IsSupported(name string) bool {
36+
return name == GatewayClass ||
37+
name == Gateway ||
38+
name == HTTPRoute ||
39+
name == ReferenceGrant
40+
}

projects/gateway2/wellknown/gwapi.go

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ const (
3232
)
3333

3434
var (
35+
// SupportedVersions specifies the supported versions of Gateway API.
36+
// [TODO] Remove "v1.0.0-rc1" when https://github.com/solo-io/gloo/issues/10115 is fixed.
37+
SupportedVersions = []string{"v1.1.0", "v1.0.0", "v1.0.0-rc1"}
38+
3539
GatewayGVK = schema.GroupVersionKind{
3640
Group: GatewayGroup,
3741
Version: "v1",

projects/gloo/pkg/api/v1/kube/wellknown/types.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ package wellknown
33
import (
44
appsv1 "k8s.io/api/apps/v1"
55
corev1 "k8s.io/api/core/v1"
6+
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
67
)
78

89
var (
910
SecretGVK = corev1.SchemeGroupVersion.WithKind("Secret")
1011
ConfigMapGVK = corev1.SchemeGroupVersion.WithKind("ConfigMap")
1112
ServiceGVK = corev1.SchemeGroupVersion.WithKind("Service")
1213
ServiceAccountGVK = corev1.SchemeGroupVersion.WithKind("ServiceAccount")
13-
14-
DeploymentGVK = appsv1.SchemeGroupVersion.WithKind("Deployment")
14+
CrdGVK = apiextv1.SchemeGroupVersion.WithKind("CustomResourceDefinition")
15+
DeploymentGVK = appsv1.SchemeGroupVersion.WithKind("Deployment")
1516
)

projects/gloo/pkg/servers/iosnapshot/history_test.go

+24
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
wellknownkube "github.com/solo-io/gloo/projects/gloo/pkg/api/v1/kube/wellknown"
1414

1515
corev1 "k8s.io/api/core/v1"
16+
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1617

1718
skmatchers "github.com/solo-io/solo-kit/test/matchers"
1819

@@ -186,6 +187,16 @@ var _ = Describe("History", func() {
186187
"key": "value",
187188
},
188189
},
190+
&apiextv1.CustomResourceDefinition{
191+
ObjectMeta: metav1.ObjectMeta{
192+
Name: "kube-crd",
193+
Namespace: "crd",
194+
ManagedFields: []metav1.ManagedFieldsEntry{{
195+
Manager: "manager",
196+
}},
197+
},
198+
Spec: apiextv1.CustomResourceDefinitionSpec{},
199+
},
189200
&apiv1.Gateway{
190201
ObjectMeta: metav1.ObjectMeta{
191202
Name: "kube-gw",
@@ -420,6 +431,19 @@ var _ = Describe("History", func() {
420431
), fmt.Sprintf("results should contain %v %s.%s", wellknownkube.ConfigMapGVK, "configmap", "kube-configmap"))
421432
})
422433

434+
It("Includes CustomResourceDefinitions", func() {
435+
returnedResources := getInputSnapshotObjects(ctx, history)
436+
Expect(returnedResources).To(ContainElement(
437+
matchers.MatchClientObject(
438+
wellknownkube.CrdGVK,
439+
types.NamespacedName{
440+
Name: "kube-crd",
441+
Namespace: "crd",
442+
},
443+
),
444+
), fmt.Sprintf("results should contain %v %s.%s", wellknownkube.CrdGVK, "crd", "kube-crd"))
445+
})
446+
423447
})
424448

425449
Context("Kubernetes Gateway API Resources", func() {

projects/gloo/pkg/servers/iosnapshot/resources.go

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ var (
1717
wellknownkube.ConfigMapGVK,
1818
}
1919

20+
KubernetesExtGVKs = []schema.GroupVersionKind{
21+
wellknownkube.CrdGVK,
22+
}
23+
2024
GlooGVKs = []schema.GroupVersionKind{
2125
gloov1.SettingsGVK,
2226
gloov1.UpstreamGVK,
@@ -68,5 +72,6 @@ var (
6872
EdgeOnlyInputSnapshotGVKs,
6973
KubernetesGatewayGVKs,
7074
KubernetesGatewayIntegrationPolicyGVKs,
75+
KubernetesExtGVKs,
7176
)
7277
)

0 commit comments

Comments
 (0)