From 16b425d5870e56c9a4eadf3262b2aa34370d2f80 Mon Sep 17 00:00:00 2001 From: danehans Date: Mon, 12 Dec 2022 10:45:27 -0800 Subject: [PATCH] Adds AuthenticationFilter Support to Kube Provider Signed-off-by: danehans --- .gitignore | 1 + api/v1alpha1/authenticationfilter_types.go | 11 +- api/v1alpha1/zz_generated.deepcopy.go | 48 ++-- internal/envoygateway/scheme.go | 10 +- internal/gatewayapi/helpers.go | 30 +++ internal/gatewayapi/helpers_test.go | 125 +++++++++++ internal/gatewayapi/translator.go | 15 ++ internal/gatewayapi/zz_generated.deepcopy.go | 12 + .../kubernetes/config/crd/kustomization.yaml | 1 + .../provider/kubernetes/config/rbac/role.yaml | 9 + internal/provider/kubernetes/controller.go | 121 +++++++--- .../provider/kubernetes/kubernetes_test.go | 64 +++++- internal/provider/kubernetes/predicates.go | 23 ++ internal/provider/kubernetes/rbac.go | 1 + internal/provider/kubernetes/routes.go | 31 ++- .../{route_test.go => routes_test.go} | 212 ++++++++++++++++++ internal/provider/kubernetes/test/utils.go | 30 +++ 17 files changed, 678 insertions(+), 66 deletions(-) create mode 100644 internal/gatewayapi/helpers_test.go rename internal/provider/kubernetes/{route_test.go => routes_test.go} (61%) diff --git a/.gitignore b/.gitignore index 24c1a01fee..83c31a78a7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ release-artifacts/ # Outputs coverage.xml +cover.out # `go mod vendor` vendor/ diff --git a/api/v1alpha1/authenticationfilter_types.go b/api/v1alpha1/authenticationfilter_types.go index 8770c2fb2b..f897b49fac 100644 --- a/api/v1alpha1/authenticationfilter_types.go +++ b/api/v1alpha1/authenticationfilter_types.go @@ -9,6 +9,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const ( + // AuthenticationFilterKind is the name of the AuthenticationFilter kind. + AuthenticationFilterKind = "AuthenticationFilter" +) + //+kubebuilder:object:root=true type AuthenticationFilter struct { @@ -114,13 +119,13 @@ type RemoteJWKS struct { //+kubebuilder:object:root=true -// AuthenticationList contains a list of Authentication. -type AuthenticationList struct { +// AuthenticationFilterList contains a list of AuthenticationFilter. +type AuthenticationFilterList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []AuthenticationFilter `json:"items"` } func init() { - SchemeBuilder.Register(&AuthenticationFilter{}, &AuthenticationList{}) + SchemeBuilder.Register(&AuthenticationFilter{}, &AuthenticationFilterList{}) } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8d470935d8..52f08bc075 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -41,59 +41,59 @@ func (in *AuthenticationFilter) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AuthenticationFilterSpec) DeepCopyInto(out *AuthenticationFilterSpec) { +func (in *AuthenticationFilterList) DeepCopyInto(out *AuthenticationFilterList) { *out = *in - if in.JwtProviders != nil { - in, out := &in.JwtProviders, &out.JwtProviders - *out = make([]JwtAuthenticationFilterProvider, len(*in)) + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AuthenticationFilter, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationFilterSpec. -func (in *AuthenticationFilterSpec) DeepCopy() *AuthenticationFilterSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationFilterList. +func (in *AuthenticationFilterList) DeepCopy() *AuthenticationFilterList { if in == nil { return nil } - out := new(AuthenticationFilterSpec) + out := new(AuthenticationFilterList) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AuthenticationFilterList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AuthenticationList) DeepCopyInto(out *AuthenticationList) { +func (in *AuthenticationFilterSpec) DeepCopyInto(out *AuthenticationFilterSpec) { *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]AuthenticationFilter, len(*in)) + if in.JwtProviders != nil { + in, out := &in.JwtProviders, &out.JwtProviders + *out = make([]JwtAuthenticationFilterProvider, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationList. -func (in *AuthenticationList) DeepCopy() *AuthenticationList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationFilterSpec. +func (in *AuthenticationFilterSpec) DeepCopy() *AuthenticationFilterSpec { if in == nil { return nil } - out := new(AuthenticationList) + out := new(AuthenticationFilterSpec) in.DeepCopyInto(out) return out } -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AuthenticationList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JwtAuthenticationFilterProvider) DeepCopyInto(out *JwtAuthenticationFilterProvider) { *out = *in diff --git a/internal/envoygateway/scheme.go b/internal/envoygateway/scheme.go index 085d69eea8..8f175ee21c 100644 --- a/internal/envoygateway/scheme.go +++ b/internal/envoygateway/scheme.go @@ -11,7 +11,8 @@ import ( gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" - "github.com/envoyproxy/gateway/api/config/v1alpha1" + egcfgv1a1 "github.com/envoyproxy/gateway/api/config/v1alpha1" + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" ) var ( @@ -28,9 +29,14 @@ func init() { if err := clientgoscheme.AddToScheme(scheme); err != nil { panic(err) } - if err := v1alpha1.AddToScheme(scheme); err != nil { + // Add Envoy Gateway types. + if err := egcfgv1a1.AddToScheme(scheme); err != nil { panic(err) } + if err := egv1a1.AddToScheme(scheme); err != nil { + panic(err) + } + // Add Gateway API types. if err := gwapiv1b1.AddToScheme(scheme); err != nil { panic(err) } diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index b374359ece..30e7e1c52b 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -6,11 +6,15 @@ package gatewayapi import ( + "errors" + "fmt" "strings" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/apis/v1beta1" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" ) const ( @@ -246,3 +250,29 @@ func layer4Protocol(protocolPort *ProtocolPort) string { return UDPProtocol } } + +// ValidateHTTPRouteFilter validates the provided filter. +func ValidateHTTPRouteFilter(filter *v1beta1.HTTPRouteFilter) error { + switch { + case filter == nil: + return errors.New("filter is nil") + case filter.Type == v1beta1.HTTPRouteFilterRequestMirror || + filter.Type == v1beta1.HTTPRouteFilterURLRewrite || + filter.Type == v1beta1.HTTPRouteFilterRequestRedirect || + filter.Type == v1beta1.HTTPRouteFilterRequestHeaderModifier: + return nil + case filter.Type == v1beta1.HTTPRouteFilterExtensionRef: + switch { + case filter.ExtensionRef == nil: + return errors.New("extensionRef field must be specified for an extended filter") + case string(filter.ExtensionRef.Group) != egv1a1.GroupVersion.Group: + return fmt.Errorf("invalid group; must be %s", egv1a1.GroupVersion.Group) + case string(filter.ExtensionRef.Kind) != egv1a1.AuthenticationFilterKind: + return fmt.Errorf("invalid kind; must be %s", egv1a1.AuthenticationFilterKind) + default: + return nil + } + } + + return fmt.Errorf("unsupported filter type: %v", filter.Type) +} diff --git a/internal/gatewayapi/helpers_test.go b/internal/gatewayapi/helpers_test.go new file mode 100644 index 0000000000..116f1299e8 --- /dev/null +++ b/internal/gatewayapi/helpers_test.go @@ -0,0 +1,125 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +// This file contains code derived from Contour, +// https://github.com/projectcontour/contour +// and is provided here subject to the following: +// Copyright Project Contour Authors +// SPDX-License-Identifier: Apache-2.0 + +package gatewayapi + +import ( + "testing" + + "github.com/stretchr/testify/require" + gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" +) + +func TestValidateAuthenFilterRef(t *testing.T) { + testCases := []struct { + name string + filter *gwapiv1b1.HTTPRouteFilter + expected bool + }{ + { + name: "request mirror filter", + filter: &gwapiv1b1.HTTPRouteFilter{ + Type: gwapiv1b1.HTTPRouteFilterRequestMirror, + }, + expected: true, + }, + { + name: "url rewrite filter", + filter: &gwapiv1b1.HTTPRouteFilter{ + Type: gwapiv1b1.HTTPRouteFilterURLRewrite, + }, + expected: true, + }, + { + name: "request header modifier filter", + filter: &gwapiv1b1.HTTPRouteFilter{ + Type: gwapiv1b1.HTTPRouteFilterRequestHeaderModifier, + }, + expected: true, + }, + { + name: "request redirect filter", + filter: &gwapiv1b1.HTTPRouteFilter{ + Type: gwapiv1b1.HTTPRouteFilterRequestRedirect, + }, + expected: true, + }, + { + name: "unsupported extended filter", + filter: &gwapiv1b1.HTTPRouteFilter{ + Type: gwapiv1b1.HTTPRouteFilterExtensionRef, + ExtensionRef: &gwapiv1b1.LocalObjectReference{ + Group: "UnsupportedGroup", + Kind: "UnsupportedKind", + Name: "test", + }, + }, + expected: false, + }, + { + name: "extended filter with missing reference", + filter: &gwapiv1b1.HTTPRouteFilter{ + Type: gwapiv1b1.HTTPRouteFilterExtensionRef, + }, + expected: false, + }, + { + name: "invalid authenticationfilter group", + filter: &gwapiv1b1.HTTPRouteFilter{ + Type: gwapiv1b1.HTTPRouteFilterExtensionRef, + ExtensionRef: &gwapiv1b1.LocalObjectReference{ + Group: "UnsupportedGroup", + Kind: egv1a1.AuthenticationFilterKind, + Name: "test", + }, + }, + expected: false, + }, + { + name: "invalid authenticationfilter kind", + filter: &gwapiv1b1.HTTPRouteFilter{ + Type: gwapiv1b1.HTTPRouteFilterExtensionRef, + ExtensionRef: &gwapiv1b1.LocalObjectReference{ + Group: gwapiv1b1.Group(egv1a1.GroupVersion.Group), + Kind: "UnsupportedKind", + Name: "test", + }, + }, + expected: false, + }, + { + name: "valid authenticationfilter", + filter: &gwapiv1b1.HTTPRouteFilter{ + Type: gwapiv1b1.HTTPRouteFilterExtensionRef, + ExtensionRef: &gwapiv1b1.LocalObjectReference{ + Group: gwapiv1b1.Group(egv1a1.GroupVersion.Group), + Kind: egv1a1.AuthenticationFilterKind, + Name: "test", + }, + }, + expected: true, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + err := ValidateHTTPRouteFilter(tc.filter) + if tc.expected { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } +} diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go index 5048c82099..c9d1494b84 100644 --- a/internal/gatewayapi/translator.go +++ b/internal/gatewayapi/translator.go @@ -17,6 +17,7 @@ import ( "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/apis/v1beta1" + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/ir" ) @@ -58,6 +59,20 @@ type Resources struct { Namespaces []*v1.Namespace Services []*v1.Service Secrets []*v1.Secret + AuthenFilters []*egv1a1.AuthenticationFilter +} + +func NewResources() *Resources { + return &Resources{ + Gateways: []*v1beta1.Gateway{}, + HTTPRoutes: []*v1beta1.HTTPRoute{}, + TLSRoutes: []*v1alpha2.TLSRoute{}, + Services: []*v1.Service{}, + Secrets: []*v1.Secret{}, + ReferenceGrants: []*v1alpha2.ReferenceGrant{}, + Namespaces: []*v1.Namespace{}, + AuthenFilters: []*egv1a1.AuthenticationFilter{}, + } } func (r *Resources) GetNamespace(name string) *v1.Namespace { diff --git a/internal/gatewayapi/zz_generated.deepcopy.go b/internal/gatewayapi/zz_generated.deepcopy.go index 1086414562..052c14eb6c 100644 --- a/internal/gatewayapi/zz_generated.deepcopy.go +++ b/internal/gatewayapi/zz_generated.deepcopy.go @@ -11,6 +11,7 @@ package gatewayapi import ( + "github.com/envoyproxy/gateway/api/v1alpha1" "k8s.io/api/core/v1" "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/apis/v1beta1" @@ -107,6 +108,17 @@ func (in *Resources) DeepCopyInto(out *Resources) { } } } + if in.AuthenFilters != nil { + in, out := &in.AuthenFilters, &out.AuthenFilters + *out = make([]*v1alpha1.AuthenticationFilter, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(v1alpha1.AuthenticationFilter) + (*in).DeepCopyInto(*out) + } + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resources. diff --git a/internal/provider/kubernetes/config/crd/kustomization.yaml b/internal/provider/kubernetes/config/crd/kustomization.yaml index c95a949ebc..1d85bb9d28 100644 --- a/internal/provider/kubernetes/config/crd/kustomization.yaml +++ b/internal/provider/kubernetes/config/crd/kustomization.yaml @@ -3,6 +3,7 @@ # It should be run by config/default resources: - bases/config.gateway.envoyproxy.io_envoyproxies.yaml +- bases/gateway.envoyproxy.io_authenticationfilters.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: diff --git a/internal/provider/kubernetes/config/rbac/role.yaml b/internal/provider/kubernetes/config/rbac/role.yaml index 66a48f7f1d..a8a376c10d 100644 --- a/internal/provider/kubernetes/config/rbac/role.yaml +++ b/internal/provider/kubernetes/config/rbac/role.yaml @@ -23,6 +23,15 @@ rules: - get - list - watch +- apiGroups: + - gateway.envoyproxy.io + resources: + - authenticationfilters + verbs: + - get + - list + - update + - watch - apiGroups: - gateway.networking.k8s.io resources: diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go index 522290ce53..4d51fec9da 100644 --- a/internal/provider/kubernetes/controller.go +++ b/internal/provider/kubernetes/controller.go @@ -9,13 +9,6 @@ import ( "context" "fmt" - "github.com/envoyproxy/gateway/internal/envoygateway/config" - "github.com/envoyproxy/gateway/internal/gatewayapi" - "github.com/envoyproxy/gateway/internal/message" - "github.com/envoyproxy/gateway/internal/provider/utils" - "github.com/envoyproxy/gateway/internal/status" - "github.com/envoyproxy/gateway/internal/utils/slice" - "github.com/go-logr/logr" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -32,16 +25,25 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/envoygateway/config" + "github.com/envoyproxy/gateway/internal/gatewayapi" + "github.com/envoyproxy/gateway/internal/message" + "github.com/envoyproxy/gateway/internal/provider/utils" + "github.com/envoyproxy/gateway/internal/status" + "github.com/envoyproxy/gateway/internal/utils/slice" ) const ( - classGatewayIndex = "classGatewayIndex" - gatewayTLSRouteIndex = "gatewayTLSRouteIndex" - gatewayHTTPRouteIndex = "gatewayHTTPRouteIndex" - secretGatewayIndex = "secretGatewayIndex" - targetRefGrantRouteIndex = "targetRefGrantRouteIndex" - serviceHTTPRouteIndex = "serviceHTTPRouteIndex" - serviceTLSRouteIndex = "serviceTLSRouteIndex" + classGatewayIndex = "classGatewayIndex" + gatewayTLSRouteIndex = "gatewayTLSRouteIndex" + gatewayHTTPRouteIndex = "gatewayHTTPRouteIndex" + secretGatewayIndex = "secretGatewayIndex" + targetRefGrantRouteIndex = "targetRefGrantRouteIndex" + serviceHTTPRouteIndex = "serviceHTTPRouteIndex" + serviceTLSRouteIndex = "serviceTLSRouteIndex" + authenFilterHTTPRouteIndex = "authenHTTPRouteIndex" ) type gatewayAPIReconciler struct { @@ -156,6 +158,14 @@ func newGatewayAPIController(mgr manager.Manager, cfg *config.Server, su status. return err } + // Watch AuthenticationFilter CRUDs and enqueue associated HTTPRoute objects. + if err := c.Watch( + &source.Kind{Type: &egv1a1.AuthenticationFilter{}}, + &handler.EnqueueRequestForObject{}, + predicate.NewPredicateFuncs(r.httpRoutesForAuthenticationFilter)); err != nil { + return err + } + r.log.Info("watching gatewayAPI related objects") return nil } @@ -167,6 +177,18 @@ type resourceMappings struct { allAssociatedBackendRefs map[types.NamespacedName]struct{} // Map for storing referenceGrant NamespaceNames for BackendRefs, SecretRefs. allAssociatedRefGrants map[types.NamespacedName]*gwapiv1a2.ReferenceGrant + // httpRouteToAuthenFilters is a map of httproute to authenticationfilter associations, + // where the key is the httproute namespaced name. + httpRouteToAuthenFilters map[types.NamespacedName][]*egv1a1.AuthenticationFilter +} + +func newResourceMapping() *resourceMappings { + return &resourceMappings{ + allAssociatedNamespaces: map[string]struct{}{}, + allAssociatedBackendRefs: map[types.NamespacedName]struct{}{}, + allAssociatedRefGrants: map[types.NamespacedName]*gwapiv1a2.ReferenceGrant{}, + httpRouteToAuthenFilters: map[types.NamespacedName][]*egv1a1.AuthenticationFilter{}, + } } func (r *gatewayAPIReconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { @@ -238,21 +260,9 @@ func (r *gatewayAPIReconciler) Reconcile(ctx context.Context, request reconcile. } } - resourceTree := &gatewayapi.Resources{ - Gateways: []*gwapiv1b1.Gateway{}, - HTTPRoutes: []*gwapiv1b1.HTTPRoute{}, - TLSRoutes: []*gwapiv1a2.TLSRoute{}, - Services: []*corev1.Service{}, - Secrets: []*corev1.Secret{}, - ReferenceGrants: []*gwapiv1a2.ReferenceGrant{}, - Namespaces: []*corev1.Namespace{}, - } - - resourceMap := &resourceMappings{ - allAssociatedNamespaces: map[string]struct{}{}, - allAssociatedBackendRefs: map[types.NamespacedName]struct{}{}, - allAssociatedRefGrants: map[types.NamespacedName]*gwapiv1a2.ReferenceGrant{}, - } + // Initialize resource types. + resourceTree := gatewayapi.NewResources() + resourceMap := newResourceMapping() // Find gateways for the acceptedGC // Find the Gateways that reference this Class. @@ -360,6 +370,13 @@ func (r *gatewayAPIReconciler) Reconcile(ctx context.Context, request reconcile. resourceTree.Namespaces = append(resourceTree.Namespaces, namespace) } + // Add all AuthenticationFilters to the resourceTree. + for _, hroute := range resourceTree.HTTPRoutes { + if filters, ok := resourceMap.httpRouteToAuthenFilters[utils.NamespacedName(hroute)]; ok { + resourceTree.AuthenFilters = append(resourceTree.AuthenFilters, filters...) + } + } + if err := updater(acceptedGC, true); err != nil { r.log.Error(err, "unable to update GatewayClass status") return reconcile.Result{}, err @@ -457,6 +474,19 @@ func (r *gatewayAPIReconciler) findReferenceGrant(ctx context.Context, from, to to.kind, to.namespace, from.kind, from.namespace) } +func (r *gatewayAPIReconciler) getAuthenticationFilter(ctx context.Context, ns, name string) (*egv1a1.AuthenticationFilter, error) { + filter := new(egv1a1.AuthenticationFilter) + key := types.NamespacedName{ + Namespace: ns, + Name: name, + } + if err := r.client.Get(ctx, key, filter); err != nil { + return nil, err + } + + return filter, nil +} + func addReferenceGrantIndexers(ctx context.Context, mgr manager.Manager) error { if err := mgr.GetFieldIndexer().IndexField(ctx, &gwapiv1a2.ReferenceGrant{}, targetRefGrantRouteIndex, func(rawObj client.Object) []string { refGrant := rawObj.(*gwapiv1a2.ReferenceGrant) @@ -471,9 +501,12 @@ func addReferenceGrantIndexers(ctx context.Context, mgr manager.Manager) error { return nil } -// addHTTPRouteIndexers adds indexing on HTTPRoute, for Service objects that are -// referenced in HTTPRoute objects via `.spec.rules.backendRefs`. This helps in -// querying for HTTPRoutes that are affected by a particular Service CRUD. +// addHTTPRouteIndexers adds indexing on HTTPRoute. +// - For Service objects that are referenced in HTTPRoute objects via `.spec.rules.backendRefs`. +// This helps in querying for HTTPRoutes that are affected by a particular Service CRUD. +// - For AuthenticationFilter objects that are referenced in HTTPRoute objects via +// `.spec.rules[].filters`. This helps in querying for HTTPRoutes that are affected by a +// particular AuthenticationFilter CRUD. func addHTTPRouteIndexers(ctx context.Context, mgr manager.Manager) error { if err := mgr.GetFieldIndexer().IndexField(ctx, &gwapiv1b1.HTTPRoute{}, gatewayHTTPRouteIndex, func(rawObj client.Object) []string { httproute := rawObj.(*gwapiv1b1.HTTPRoute) @@ -516,6 +549,30 @@ func addHTTPRouteIndexers(ctx context.Context, mgr manager.Manager) error { }); err != nil { return err } + + if err := mgr.GetFieldIndexer().IndexField(ctx, &gwapiv1b1.HTTPRoute{}, authenFilterHTTPRouteIndex, func(obj client.Object) []string { + httproute := obj.(*gwapiv1b1.HTTPRoute) + var filters []string + for _, rule := range httproute.Spec.Rules { + for i := range rule.Filters { + filter := rule.Filters[i] + if filter.Type == gwapiv1a2.HTTPRouteFilterExtensionRef { + if err := gatewayapi.ValidateHTTPRouteFilter(&filter); err != nil { + filters = append(filters, + types.NamespacedName{ + Namespace: httproute.Namespace, + Name: string(filter.ExtensionRef.Name), + }.String(), + ) + } + } + } + } + return filters + }); err != nil { + return err + } + return nil } diff --git a/internal/provider/kubernetes/kubernetes_test.go b/internal/provider/kubernetes/kubernetes_test.go index 39e606333f..a7d838419f 100644 --- a/internal/provider/kubernetes/kubernetes_test.go +++ b/internal/provider/kubernetes/kubernetes_test.go @@ -32,6 +32,7 @@ import ( gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/envoyproxy/gateway/api/config/v1alpha1" + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/envoygateway/config" "github.com/envoyproxy/gateway/internal/gatewayapi" "github.com/envoyproxy/gateway/internal/message" @@ -82,9 +83,10 @@ func TestProvider(t *testing.T) { func startEnv() (*envtest.Environment, *rest.Config, error) { log.SetLogger(zap.New(zap.WriteTo(os.Stderr), zap.UseDevMode(true))) - crd := filepath.Join(".", "testdata", "in") + gwAPIs := filepath.Join(".", "testdata", "in") + egAPIs := filepath.Join(".", "config", "crd", "bases") env := &envtest.Environment{ - CRDDirectoryPaths: []string{crd}, + CRDDirectoryPaths: []string{gwAPIs, egAPIs}, } cfg, err := env.Start() if err != nil { @@ -458,6 +460,14 @@ func testHTTPRoute(ctx context.Context, t *testing.T, provider *Provider, resour require.NoError(t, cli.Delete(ctx, svc)) }() + authenFilter := test.GetAuthenticationFilter("test-authen", ns.Name) + + require.NoError(t, cli.Create(ctx, authenFilter)) + + defer func() { + require.NoError(t, cli.Delete(ctx, authenFilter)) + }() + redirectHostname := gwapiv1b1.PreciseHostname("redirect.hostname.local") redirectPort := gwapiv1b1.PortNumber(8443) redirectStatus := 301 @@ -791,6 +801,56 @@ func testHTTPRoute(ctx context.Context, t *testing.T, provider *Provider, resour }, }, }, + { + name: "authenfilter-httproute", + route: gwapiv1b1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "httproute-authenfilter-test", + Namespace: ns.Name, + }, + Spec: gwapiv1b1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1b1.CommonRouteSpec{ + ParentRefs: []gwapiv1b1.ParentReference{ + { + Name: gwapiv1b1.ObjectName(gw.Name), + }, + }, + }, + Hostnames: []gwapiv1b1.Hostname{"test.hostname.local"}, + Rules: []gwapiv1b1.HTTPRouteRule{ + { + Matches: []gwapiv1b1.HTTPRouteMatch{ + { + Path: &gwapiv1b1.HTTPPathMatch{ + Type: gatewayapi.PathMatchTypePtr(gwapiv1b1.PathMatchPathPrefix), + Value: gatewayapi.StringPtr("/authenfilter/"), + }, + }, + }, + BackendRefs: []gwapiv1b1.HTTPBackendRef{ + { + BackendRef: gwapiv1b1.BackendRef{ + BackendObjectReference: gwapiv1b1.BackendObjectReference{ + Name: "test", + }, + }, + }, + }, + Filters: []gwapiv1b1.HTTPRouteFilter{ + { + Type: gwapiv1b1.HTTPRouteFilterExtensionRef, + ExtensionRef: &gwapiv1b1.LocalObjectReference{ + Group: gwapiv1b1.Group(egv1a1.GroupVersion.Group), + Kind: gwapiv1b1.Kind(egv1a1.AuthenticationFilterKind), + Name: gwapiv1b1.ObjectName("test-authen"), + }, + }, + }, + }, + }, + }, + }, + }, } for _, testCase := range testCases { diff --git a/internal/provider/kubernetes/predicates.go b/internal/provider/kubernetes/predicates.go index 78e065dd2b..5f510a60db 100644 --- a/internal/provider/kubernetes/predicates.go +++ b/internal/provider/kubernetes/predicates.go @@ -17,6 +17,7 @@ import ( gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/gatewayapi" "github.com/envoyproxy/gateway/internal/provider/utils" ) @@ -180,6 +181,28 @@ func (r *gatewayAPIReconciler) validateDeploymentForReconcile(obj client.Object) return false } +// httpRoutesForAuthenticationFilter tries finding HTTPRoute referents of the provided +// AuthenticationFilter and returns true if any exist. +func (r *gatewayAPIReconciler) httpRoutesForAuthenticationFilter(obj client.Object) bool { + ctx := context.Background() + filter, ok := obj.(*egv1a1.AuthenticationFilter) + if !ok { + r.log.Info("unexpected object type, bypassing reconciliation", "object", obj) + return false + } + + // Check if the AuthenticationFilter belongs to a managed HTTPRoute. + httpRouteList := &gwapiv1b1.HTTPRouteList{} + if err := r.client.List(ctx, httpRouteList, &client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(authenFilterHTTPRouteIndex, utils.NamespacedName(filter).String()), + }); err != nil { + r.log.Error(err, "unable to find associated HTTPRoutes") + return false + } + + return len(httpRouteList.Items) != 0 +} + // envoyDeploymentForGateway returns the Envoy Deployment, returning nil if the Deployment doesn't exist. func (r *gatewayAPIReconciler) envoyDeploymentForGateway(ctx context.Context, gateway *gwapiv1b1.Gateway) (*appsv1.Deployment, error) { key := types.NamespacedName{ diff --git a/internal/provider/kubernetes/rbac.go b/internal/provider/kubernetes/rbac.go index 8222daec3e..b37e577c0a 100644 --- a/internal/provider/kubernetes/rbac.go +++ b/internal/provider/kubernetes/rbac.go @@ -7,6 +7,7 @@ package kubernetes // +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses;gateways;httproutes;tlsroutes;referencepolicies;referencegrants,verbs=get;list;watch;update // +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses/status;gateways/status;httproutes/status;tlsroutes/status,verbs=update +// +kubebuilder:rbac:groups="gateway.envoyproxy.io",resources=authenticationfilters,verbs=get;list;watch;update // RBAC for watched resources of Gateway API controllers. // +kubebuilder:rbac:groups="",resources=secrets;services;namespaces,verbs=get;list;watch diff --git a/internal/provider/kubernetes/routes.go b/internal/provider/kubernetes/routes.go index b13a494f81..a1e840faec 100644 --- a/internal/provider/kubernetes/routes.go +++ b/internal/provider/kubernetes/routes.go @@ -8,13 +8,15 @@ package kubernetes import ( "context" - "github.com/envoyproxy/gateway/internal/gatewayapi" - "github.com/envoyproxy/gateway/internal/provider/utils" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/gatewayapi" + "github.com/envoyproxy/gateway/internal/provider/utils" ) // processTLSRoutes finds TLSRoutes corresponding to a gatewayNamespaceName, further checks for @@ -77,13 +79,14 @@ func (r *gatewayAPIReconciler) processHTTPRoutes(ctx context.Context, gatewayNam if err := r.client.List(ctx, httpRouteList, &client.ListOptions{ FieldSelector: fields.OneTermEqualSelector(gatewayHTTPRouteIndex, gatewayNamespaceName), }); err != nil { - r.log.Error(err, "unable to find associated HTTPRoutes") + r.log.Error(err, "failed to list HTTPRoutes") return err } for _, httpRoute := range httpRouteList.Items { httpRoute := httpRoute r.log.Info("processing HTTPRoute", "namespace", httpRoute.Namespace, "name", httpRoute.Name) + var authenFilters []*egv1a1.AuthenticationFilter for _, rule := range httpRoute.Spec.Rules { for _, backendRef := range rule.BackendRefs { backendRef := backendRef @@ -110,6 +113,28 @@ func (r *gatewayAPIReconciler) processHTTPRoutes(ctx context.Context, gatewayNam resourceMap.allAssociatedRefGrants[utils.NamespacedName(refGrant)] = refGrant } } + + for i := range rule.Filters { + filter := rule.Filters[i] + if err := gatewayapi.ValidateHTTPRouteFilter(&filter); err != nil { + r.log.Error(err, "bypassing filter rule", "index", i) + continue + } + + if filter.Type == gwapiv1b1.HTTPRouteFilterExtensionRef { + authenFilter, err := r.getAuthenticationFilter(ctx, httpRoute.Namespace, string(filter.ExtensionRef.Name)) + if err != nil { + r.log.Error(err, "bypassing filter rule", "index", i) + continue + } + + authenFilters = append(authenFilters, authenFilter) + } + } + } + + if len(authenFilters) > 0 { + resourceMap.httpRouteToAuthenFilters[utils.NamespacedName(&httpRoute)] = authenFilters } resourceMap.allAssociatedNamespaces[httpRoute.Namespace] = struct{}{} diff --git a/internal/provider/kubernetes/route_test.go b/internal/provider/kubernetes/routes_test.go similarity index 61% rename from internal/provider/kubernetes/route_test.go rename to internal/provider/kubernetes/routes_test.go index 97a458d096..ed30490dd7 100644 --- a/internal/provider/kubernetes/route_test.go +++ b/internal/provider/kubernetes/routes_test.go @@ -11,16 +11,228 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/envoyproxy/gateway/api/config/v1alpha1" + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/envoygateway" "github.com/envoyproxy/gateway/internal/gatewayapi" + "github.com/envoyproxy/gateway/internal/log" + "github.com/envoyproxy/gateway/internal/provider/utils" ) +func TestProcessHTTPRoutes(t *testing.T) { + // The gatewayclass configured for the reconciler and referenced by test cases. + gcCtrlName := gwapiv1b1.GatewayController(v1alpha1.GatewayControllerName) + gc := &gwapiv1b1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: gwapiv1b1.GatewayClassSpec{ + ControllerName: gcCtrlName, + }, + } + + // The gateway referenced by test cases. + gw := &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: gwapiv1b1.ObjectName(gc.Name), + Listeners: []gwapiv1b1.Listener{ + { + Name: "http", + Protocol: gwapiv1b1.HTTPProtocolType, + Port: gwapiv1b1.PortNumber(int32(8080)), + }, + }, + }, + } + gwNsName := utils.NamespacedName(gw).String() + + testCases := []struct { + name string + routes []*gwapiv1b1.HTTPRoute + filters []*egv1a1.AuthenticationFilter + expected bool + }{ + { + name: "valid httproute", + routes: []*gwapiv1b1.HTTPRoute{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: gwapiv1b1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1b1.CommonRouteSpec{ + ParentRefs: []gwapiv1b1.ParentReference{ + { + Name: "test", + }, + }, + }, + Rules: []gwapiv1b1.HTTPRouteRule{ + { + Matches: []gwapiv1b1.HTTPRouteMatch{ + { + Path: &gwapiv1b1.HTTPPathMatch{ + Type: gatewayapi.PathMatchTypePtr(gwapiv1b1.PathMatchPathPrefix), + Value: gatewayapi.StringPtr("/"), + }, + }, + }, + BackendRefs: []gwapiv1b1.HTTPBackendRef{ + { + BackendRef: gwapiv1b1.BackendRef{ + BackendObjectReference: gwapiv1b1.BackendObjectReference{ + Group: gatewayapi.GroupPtr(corev1.GroupName), + Kind: gatewayapi.KindPtr(gatewayapi.KindService), + Name: "test", + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + { + name: "httproute with one authenticationfilter", + routes: []*gwapiv1b1.HTTPRoute{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: gwapiv1b1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1b1.CommonRouteSpec{ + ParentRefs: []gwapiv1b1.ParentReference{ + { + Name: "test", + }, + }, + }, + Rules: []gwapiv1b1.HTTPRouteRule{ + { + Matches: []gwapiv1b1.HTTPRouteMatch{ + { + Path: &gwapiv1b1.HTTPPathMatch{ + Type: gatewayapi.PathMatchTypePtr(gwapiv1b1.PathMatchPathPrefix), + Value: gatewayapi.StringPtr("/"), + }, + }, + }, + Filters: []gwapiv1b1.HTTPRouteFilter{ + { + Type: gwapiv1b1.HTTPRouteFilterExtensionRef, + ExtensionRef: &gwapiv1b1.LocalObjectReference{ + Group: gwapiv1b1.Group(egv1a1.GroupVersion.Group), + Kind: gwapiv1b1.Kind(egv1a1.AuthenticationFilterKind), + Name: gwapiv1b1.ObjectName("test"), + }, + }, + }, + BackendRefs: []gwapiv1b1.HTTPBackendRef{ + { + BackendRef: gwapiv1b1.BackendRef{ + BackendObjectReference: gwapiv1b1.BackendObjectReference{ + Group: gatewayapi.GroupPtr(corev1.GroupName), + Kind: gatewayapi.KindPtr(gatewayapi.KindService), + Name: "test", + }, + }, + }, + }, + }, + }, + }, + }, + }, + filters: []*egv1a1.AuthenticationFilter{ + { + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.AuthenticationFilterKind, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.AuthenticationFilterSpec{ + Type: egv1a1.JwtAuthenticationFilterProviderType, + JwtProviders: []egv1a1.JwtAuthenticationFilterProvider{ + { + Name: "test", + Issuer: "https://www.test.local", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + }, + }, + }, + }, + }, + expected: true, + }, + } + + for i := range testCases { + tc := testCases[i] + + // Add objects referenced by test cases. + objs := []client.Object{gc, gw} + + // Create the reconciler. + logger, err := log.NewLogger() + require.NoError(t, err) + r := &gatewayAPIReconciler{ + log: logger, + classController: gcCtrlName, + } + ctx := context.Background() + + // Run the test cases. + t.Run(tc.name, func(t *testing.T) { + // Add the test case objects to the reconciler client. + for _, route := range tc.routes { + objs = append(objs, route) + } + for _, filter := range tc.filters { + objs = append(objs, filter) + } + r.client = fakeclient.NewClientBuilder().WithScheme(envoygateway.GetScheme()).WithObjects(objs...).Build() + + // Process the test case httproutes. + resourceTree := gatewayapi.NewResources() + resourceMap := newResourceMapping() + err := r.processHTTPRoutes(ctx, gwNsName, resourceMap, resourceTree) + if tc.expected { + require.NoError(t, err) + // Ensure the resource tree and map are as expected. + require.Equal(t, tc.routes, resourceTree.HTTPRoutes) + if tc.filters != nil { + for _, route := range tc.routes { + require.Equal(t, tc.filters, resourceMap.httpRouteToAuthenFilters[utils.NamespacedName(route)]) + } + } + } else { + require.Error(t, err) + } + }) + } +} + func TestValidateHTTPRouteParentRefs(t *testing.T) { testCases := []struct { name string diff --git a/internal/provider/kubernetes/test/utils.go b/internal/provider/kubernetes/test/utils.go index c5fb53634e..1deee337cc 100644 --- a/internal/provider/kubernetes/test/utils.go +++ b/internal/provider/kubernetes/test/utils.go @@ -6,6 +6,7 @@ package test import ( + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -143,3 +144,32 @@ func GetService(nsname types.NamespacedName, labels map[string]string, ports map } return service } + +// GetAuthenticationFilter returns a pointer to an AuthenticationFilter with the +// provided ns/name. The AuthenticationFilter uses a JWT provider with dummy issuer, +// audiences, and remoteJWKS settings. +func GetAuthenticationFilter(name, ns string) *egv1a1.AuthenticationFilter { + return &egv1a1.AuthenticationFilter{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.AuthenticationFilterKind, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns, + Name: name, + }, + Spec: egv1a1.AuthenticationFilterSpec{ + Type: egv1a1.JwtAuthenticationFilterProviderType, + JwtProviders: []egv1a1.JwtAuthenticationFilterProvider{ + { + Name: "test", + Issuer: "https://www.test.local", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + }, + }, + }, + } +}