diff --git a/config/200-clusterrole.yaml b/config/200-clusterrole.yaml index f8b671d9d..be31a40cf 100644 --- a/config/200-clusterrole.yaml +++ b/config/200-clusterrole.yaml @@ -39,5 +39,8 @@ metadata: app.kubernetes.io/version: devel rules: - apiGroups: ["gateway.networking.k8s.io"] - resources: ["httproutes", "gateways"] + resources: ["httproutes", "referencegrants", "referencepolicies"] verbs: ["get", "list", "create", "update", "delete", "patch", "watch"] + - apiGroups: ["gateway.networking.k8s.io"] + resources: ["gateways"] + verbs: ["get", "list", "update", "patch", "watch"] diff --git a/docs/test-version.md b/docs/test-version.md index daf867c1a..98697dcdd 100644 --- a/docs/test-version.md +++ b/docs/test-version.md @@ -15,5 +15,5 @@ The following Gateway API version and Ingress were tested as part of the release | Ingress | Tested version | Unavailable features | | ------- | ----------------------- | ------------------------------ | -| Istio | v1.13.2 | tls,retry,httpoption,host-rewrite | -| Contour | v1.21.0 | tls,retry,httpoption,basics/http2,websocket,websocket/split,grpc,grpc/split,visibility/path,visibility,update,host-rewrite | +| Istio | v1.13.2 | retry,httpoption,host-rewrite | +| Contour | v1.21.0 | retry,httpoption,basics/http2,websocket,websocket/split,grpc,grpc/split,visibility/path,visibility,update,host-rewrite | diff --git a/hack/test-env.sh b/hack/test-env.sh index 8b4d26625..2157137fd 100755 --- a/hack/test-env.sh +++ b/hack/test-env.sh @@ -16,6 +16,6 @@ export GATEWAY_API_VERSION="v0.5.0-rc1" export ISTIO_VERSION="1.13.2" -export ISTIO_UNSUPPORTED_E2E_TESTS="tls,retry,httpoption,host-rewrite" +export ISTIO_UNSUPPORTED_E2E_TESTS="retry,httpoption,host-rewrite" export CONTOUR_VERSION="v1.21.0" -export CONTOUR_UNSUPPORTED_E2E_TESTS="tls,retry,httpoption,basics/http2,websocket,websocket/split,grpc,grpc/split,visibility/path,visibility,update,host-rewrite" +export CONTOUR_UNSUPPORTED_E2E_TESTS="retry,httpoption,basics/http2,websocket,websocket/split,grpc,grpc/split,visibility/path,visibility,update,host-rewrite" diff --git a/pkg/reconciler/ingress/controller.go b/pkg/reconciler/ingress/controller.go index 47168f867..c5db4b65b 100644 --- a/pkg/reconciler/ingress/controller.go +++ b/pkg/reconciler/ingress/controller.go @@ -37,6 +37,7 @@ import ( gwapiclient "knative.dev/net-gateway-api/pkg/client/gatewayapi/injection/client" gatewayinformer "knative.dev/net-gateway-api/pkg/client/gatewayapi/injection/informers/apis/v1alpha2/gateway" httprouteinformer "knative.dev/net-gateway-api/pkg/client/gatewayapi/injection/informers/apis/v1alpha2/httproute" + referencepolicyinformer "knative.dev/net-gateway-api/pkg/client/gatewayapi/injection/informers/apis/v1alpha2/referencepolicy" "knative.dev/net-gateway-api/pkg/reconciler/ingress/config" ) @@ -55,12 +56,15 @@ func NewController( ingressInformer := ingressinformer.Get(ctx) httprouteInformer := httprouteinformer.Get(ctx) + referencePolicyInformer := referencepolicyinformer.Get(ctx) gatewayInformer := gatewayinformer.Get(ctx) endpointsInformer := endpointsinformer.Get(ctx) c := &Reconciler{ - gwapiclient: gwapiclient.Get(ctx), - httprouteLister: httprouteInformer.Lister(), + gwapiclient: gwapiclient.Get(ctx), + httprouteLister: httprouteInformer.Lister(), + referencePolicyLister: referencePolicyInformer.Lister(), + gatewayLister: gatewayInformer.Lister(), } filterFunc := reconciler.AnnotationFilterFunc(networking.IngressClassAnnotationKey, gatewayAPIIngressClassName, false) diff --git a/pkg/reconciler/ingress/controller_test.go b/pkg/reconciler/ingress/controller_test.go index 04ce042da..eebaad901 100644 --- a/pkg/reconciler/ingress/controller_test.go +++ b/pkg/reconciler/ingress/controller_test.go @@ -32,6 +32,7 @@ import ( _ "knative.dev/net-gateway-api/pkg/client/gatewayapi/injection/informers/apis/v1alpha2/gateway/fake" _ "knative.dev/net-gateway-api/pkg/client/gatewayapi/injection/informers/apis/v1alpha2/httproute/fake" + _ "knative.dev/net-gateway-api/pkg/client/gatewayapi/injection/informers/apis/v1alpha2/referencepolicy/fake" "knative.dev/net-gateway-api/pkg/reconciler/ingress/config" ) diff --git a/pkg/reconciler/ingress/ingress.go b/pkg/reconciler/ingress/ingress.go index 2474fe5ac..15d8961b2 100644 --- a/pkg/reconciler/ingress/ingress.go +++ b/pkg/reconciler/ingress/ingress.go @@ -27,7 +27,6 @@ import ( ingressreconciler "knative.dev/networking/pkg/client/injection/reconciler/networking/v1alpha1/ingress" "knative.dev/networking/pkg/ingress" "knative.dev/networking/pkg/status" - "knative.dev/pkg/logging" "knative.dev/pkg/network" pkgreconciler "knative.dev/pkg/reconciler" @@ -49,6 +48,10 @@ type Reconciler struct { // Listers index properties about resources httprouteLister gatewayListers.HTTPRouteLister + + referencePolicyLister gatewayListers.ReferencePolicyLister + + gatewayLister gatewayListers.GatewayLister } var ( @@ -58,6 +61,7 @@ var ( // ReconcileKind implements Interface.ReconcileKind. func (c *Reconciler) ReconcileKind(ctx context.Context, ingress *v1alpha1.Ingress) pkgreconciler.Event { reconcileErr := c.reconcileIngress(ctx, ingress) + if reconcileErr != nil { ingress.Status.MarkIngressNotReady(notReconciledReason, notReconciledMessage) return reconcileErr @@ -66,8 +70,16 @@ func (c *Reconciler) ReconcileKind(ctx context.Context, ingress *v1alpha1.Ingres return nil } +// FinalizeKind implements Interface.FinalizeKind +func (c *Reconciler) FinalizeKind(ctx context.Context, ingress *v1alpha1.Ingress) pkgreconciler.Event { + gatewayConfig := config.FromContext(ctx).Gateway + + // We currently only support TLS on the external IP + return c.clearGatewayListeners(ctx, ingress, gatewayConfig.Gateways[v1alpha1.IngressVisibilityExternalIP].Gateway) +} + func (c *Reconciler) reconcileIngress(ctx context.Context, ing *v1alpha1.Ingress) error { - logger := logging.FromContext(ctx) + gatewayConfig := config.FromContext(ctx).Gateway // We may be reading a version of the object that was stored at an older version // and may not have had all of the assumed defaults specified. This won't result @@ -82,8 +94,6 @@ func (c *Reconciler) reconcileIngress(ctx context.Context, ing *v1alpha1.Ingress return fmt.Errorf("failed to add knative probe header: %w", err) } - logger.Infof("Reconciling ingress: %#v", ing) - for _, rule := range ing.Spec.Rules { rule := rule @@ -97,17 +107,37 @@ func (c *Reconciler) reconcileIngress(ctx context.Context, ing *v1alpha1.Ingress } else { ing.Status.MarkIngressNotReady("HTTPRouteNotReady", "Waiting for HTTPRoute becomes Ready.") } - logger.Infof("HTTPRoute successfully synced %v", httproutes) } + listeners := make([]*gatewayv1alpha2.Listener, 0, len(ing.Spec.TLS)) + for _, tls := range ing.Spec.TLS { + tls := tls + + l, err := c.reconcileTLS(ctx, &tls, ing) + if err != nil { + return err + } + listeners = append(listeners, l...) + } + + if len(listeners) > 0 { + // For now, we only reconcile the external visibility, because there's + // no way to provide TLS for internal listeners. + err := c.reconcileGatewayListeners( + ctx, listeners, ing, *gatewayConfig.Gateways[v1alpha1.IngressVisibilityExternalIP].Gateway) + if err != nil { + return err + } + } + + // TODO: check Gateway readiness before reporting Ingress ready + ready, err := c.statusManager.IsReady(ctx, before) if err != nil { return fmt.Errorf("failed to probe Ingress: %w", err) } if ready { - gatewayConfig := config.FromContext(ctx).Gateway - namespacedNameService := gatewayConfig.Gateways[v1alpha1.IngressVisibilityExternalIP].Service publicLbs := []v1alpha1.LoadBalancerIngressStatus{ {DomainInternal: network.GetServiceHostname(namespacedNameService.Name, namespacedNameService.Namespace)}, diff --git a/pkg/reconciler/ingress/ingress_test.go b/pkg/reconciler/ingress/ingress_test.go index 370d67ba5..393386a45 100644 --- a/pkg/reconciler/ingress/ingress_test.go +++ b/pkg/reconciler/ingress/ingress_test.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" clientgotesting "k8s.io/client-go/testing" + "k8s.io/utils/pointer" gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "knative.dev/networking/pkg/apis/networking" @@ -171,14 +172,22 @@ func TestReconcile(t *testing.T) { }}) }), }}, + WantPatches: []clientgotesting.PatchActionImpl{{ + ActionImpl: clientgotesting.ActionImpl{ + Namespace: "ns", + }, + Name: "name", + Patch: []byte(`{"metadata":{"finalizers":["ingresses.networking.internal.knative.dev"],"resourceVersion":""}}`), + }}, WantEvents: []string{ + Eventf(corev1.EventTypeNormal, "FinalizerUpdate", `Updated "name" finalizers`), Eventf(corev1.EventTypeNormal, "Created", "Created HTTPRoute \"example.com\""), }, }, { Name: "reconcile ready ingress", Key: "ns/name", Objects: append([]runtime.Object{ - ing(withBasicSpec, withGatewayAPIclass, makeItReady), + ing(withBasicSpec, withGatewayAPIclass, makeItReady, withFinalizer), httpRoute(t, ing(withBasicSpec, withGatewayAPIclass)), }, servicesAndEndpoints...), // no extra update @@ -189,6 +198,7 @@ func TestReconcile(t *testing.T) { gwapiclient: fakegwapiclientset.Get(ctx), // Listers index properties about resources httprouteLister: listers.GetHTTPRouteLister(), + gatewayLister: listers.GetGatewayLister(), statusManager: &fakeStatusManager{ FakeIsReady: func(context.Context, *v1alpha1.Ingress) (bool, error) { return true, nil @@ -221,7 +231,15 @@ func TestReconcileProberNotReady(t *testing.T) { i.Status.MarkLoadBalancerNotReady() }), }}, + WantPatches: []clientgotesting.PatchActionImpl{{ + ActionImpl: clientgotesting.ActionImpl{ + Namespace: "ns", + }, + Name: "name", + Patch: []byte(`{"metadata":{"finalizers":["ingresses.networking.internal.knative.dev"],"resourceVersion":""}}`), + }}, WantEvents: []string{ + Eventf(corev1.EventTypeNormal, "FinalizerUpdate", `Updated "name" finalizers`), Eventf(corev1.EventTypeNormal, "Created", "Created HTTPRoute \"example.com\""), }, }} @@ -231,6 +249,7 @@ func TestReconcileProberNotReady(t *testing.T) { gwapiclient: fakegwapiclientset.Get(ctx), // Listers index properties about resources httprouteLister: listers.GetHTTPRouteLister(), + gatewayLister: listers.GetGatewayLister(), statusManager: &fakeStatusManager{ FakeIsReady: func(context.Context, *v1alpha1.Ingress) (bool, error) { return false, nil @@ -246,6 +265,165 @@ func TestReconcileProberNotReady(t *testing.T) { })) } +func TestReconcileTLS(t *testing.T) { + // The gateway API annoyingly has a number of + secretName := "name-WE-STICK-A-LONG-UID-HERE" + nsName := "ns" + deleteTime := time.Now().Add(-10 * time.Second) + table := TableTest{{ + Name: "Happy TLS", + Key: "ns/name", + Objects: []runtime.Object{ + ing(withBasicSpec, withGatewayAPIClass, withTLS(secretName)), + secret(secretName, nsName), + gw(defaultListener), + }, + WantCreates: []runtime.Object{ + httpRoute(t, ing(withBasicSpec, withGatewayAPIClass, withTLS(secretName))), + rp(secret(secretName, nsName)), + }, + WantUpdates: []clientgotesting.UpdateActionImpl{{ + Object: gw(defaultListener, tlsListener("secure.example.com", nsName, secretName)), + }}, + WantPatches: []clientgotesting.PatchActionImpl{{ + ActionImpl: clientgotesting.ActionImpl{ + Namespace: "ns", + }, + Name: "name", + Patch: []byte(`{"metadata":{"finalizers":["ingresses.networking.internal.knative.dev"],"resourceVersion":""}}`), + }}, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: ing(withBasicSpec, withGatewayAPIClass, withTLS(secretName), func(i *v1alpha1.Ingress) { + i.Status.InitializeConditions() + i.Status.MarkLoadBalancerReady( + []v1alpha1.LoadBalancerIngressStatus{{ + DomainInternal: publicSvc, + }}, + []v1alpha1.LoadBalancerIngressStatus{{ + DomainInternal: privateSvc, + }}) + }), + }}, + WantEvents: []string{ + Eventf(corev1.EventTypeNormal, "FinalizerUpdate", `Updated "name" finalizers`), + Eventf(corev1.EventTypeNormal, "Created", `Created HTTPRoute "example.com"`), + }, + }, { + Name: "Already Configured", + Key: "ns/name", + Objects: []runtime.Object{ + ing(withBasicSpec, withFinalizer, withGatewayAPIClass, withTLS(secretName)), + secret(secretName, nsName), + gw(defaultListener, tlsListener("secure.example.com", nsName, secretName)), + httpRoute(t, ing(withBasicSpec, withGatewayAPIClass, withTLS(secretName))), + rp(secret(secretName, nsName)), + }, + WantUpdates: []clientgotesting.UpdateActionImpl{ + // None + }, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: ing(withBasicSpec, withFinalizer, withGatewayAPIClass, withTLS(secretName), func(i *v1alpha1.Ingress) { + i.Status.InitializeConditions() + i.Status.MarkLoadBalancerReady( + []v1alpha1.LoadBalancerIngressStatus{{ + DomainInternal: publicSvc, + }}, + []v1alpha1.LoadBalancerIngressStatus{{ + DomainInternal: privateSvc, + }}) + }), + }}, + WantEvents: []string{ + // None + }, + }, { + Name: "Cleanup Listener", + Key: "ns/name", + SkipNamespaceValidation: true, + Objects: []runtime.Object{ + ing(withBasicSpec, withGatewayAPIClass, withTLS(secretName), func(i *v1alpha1.Ingress) { + i.DeletionTimestamp = &metav1.Time{ + Time: deleteTime, + } + }), + secret(secretName, nsName), + gw(defaultListener, tlsListener("secure.example.com", nsName, secretName)), + httpRoute(t, ing(withBasicSpec, withGatewayAPIClass, withTLS(secretName))), + rp(secret(secretName, nsName)), + }, + WantUpdates: []clientgotesting.UpdateActionImpl{{ + Object: gw(defaultListener), + }}, + }, { + Name: "No Gateway", + Key: "ns/name", + WantErr: true, + Objects: []runtime.Object{ + ing(withBasicSpec, withGatewayAPIClass, withTLS(secretName)), + secret(secretName, nsName), + }, + WantCreates: []runtime.Object{ + httpRoute(t, ing(withBasicSpec, withGatewayAPIClass, withTLS(secretName))), + rp(secret(secretName, nsName)), + }, + WantUpdates: []clientgotesting.UpdateActionImpl{ + // None + }, + WantPatches: []clientgotesting.PatchActionImpl{{ + ActionImpl: clientgotesting.ActionImpl{ + Namespace: "ns", + }, + Name: "name", + Patch: []byte(`{"metadata":{"finalizers":["ingresses.networking.internal.knative.dev"],"resourceVersion":""}}`), + }}, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: ing(withBasicSpec, withGatewayAPIClass, withTLS(secretName), func(i *v1alpha1.Ingress) { + i.Status.InitializeConditions() + i.Status.MarkIngressNotReady("ReconcileIngressFailed", "Ingress reconciliation failed") + }), + }}, + WantEvents: []string{ + Eventf(corev1.EventTypeNormal, "FinalizerUpdate", `Updated "name" finalizers`), + Eventf(corev1.EventTypeNormal, "Created", `Created HTTPRoute "example.com"`), + Eventf(corev1.EventTypeWarning, "GatewayMissing", `Unable to update Gateway istio-system/istio-gateway`), + Eventf(corev1.EventTypeWarning, "InternalError", `Gateway istio-system/istio-gateway does not exist: gateway.gateway.networking.k8s.io "istio-gateway" not found`), + }, + }} + + table.Test(t, GatewayFactory(func(ctx context.Context, listers *Listers, cmw configmap.Watcher, tr *TableRow) controller.Reconciler { + r := &Reconciler{ + gwapiclient: fakegwapiclientset.Get(ctx), + httprouteLister: listers.GetHTTPRouteLister(), + referencePolicyLister: listers.GetReferencePolicyLister(), + gatewayLister: listers.GetGatewayLister(), + statusManager: &fakeStatusManager{FakeIsReady: func(context.Context, *v1alpha1.Ingress) (bool, error) { + return true, nil + }}, + } + // The fake tracker's `Add` method incorrectly pluralizes "gatewaies" using UnsafeGuessKindToResource, + // so create this via explicit call (per note in client-go/testing/fixture.go in tracker.Add) + fakeCreates := []runtime.Object{} + for _, x := range tr.Objects { + myGw, ok := x.(*gatewayv1alpha2.Gateway) + if ok { + fakegwapiclientset.Get(ctx).GatewayV1alpha2().Gateways(myGw.Namespace).Create(ctx, myGw, metav1.CreateOptions{}) + tr.SkipNamespaceValidation = true + fakeCreates = append(fakeCreates, myGw) + } + } + tr.WantCreates = append(fakeCreates, tr.WantCreates...) + + ingr := ingressreconciler.NewReconciler(ctx, logging.FromContext(ctx), fakeingressclient.Get(ctx), + listers.GetIngressLister(), controller.GetEventRecorder(ctx), r, gatewayAPIIngressClassName, + controller.Options{ + ConfigStore: &testConfigStore{ + config: defaultConfig, + }}) + + return ingr + })) +} + func TestReconcileProbeError(t *testing.T) { theError := errors.New("this is the error") @@ -263,7 +441,15 @@ func TestReconcileProbeError(t *testing.T) { i.Status.MarkIngressNotReady(notReconciledReason, notReconciledMessage) }), }}, + WantPatches: []clientgotesting.PatchActionImpl{{ + ActionImpl: clientgotesting.ActionImpl{ + Namespace: "ns", + }, + Name: "name", + Patch: []byte(`{"metadata":{"finalizers":["ingresses.networking.internal.knative.dev"],"resourceVersion":""}}`), + }}, WantEvents: []string{ + Eventf(corev1.EventTypeNormal, "FinalizerUpdate", `Updated "name" finalizers`), Eventf(corev1.EventTypeNormal, "Created", "Created HTTPRoute \"example.com\""), Eventf(corev1.EventTypeWarning, "InternalError", fmt.Sprintf("failed to probe Ingress: %v", theError)), }, @@ -274,6 +460,8 @@ func TestReconcileProbeError(t *testing.T) { gwapiclient: fakegwapiclientset.Get(ctx), // Listers index properties about resources httprouteLister: listers.GetHTTPRouteLister(), + gatewayLister: listers.GetGatewayLister(), + statusManager: &fakeStatusManager{ FakeIsReady: func(context.Context, *v1alpha1.Ingress) (bool, error) { return false, theError @@ -336,6 +524,133 @@ func (t *testConfigStore) ToContext(ctx context.Context) context.Context { return config.ToContext(ctx, t.config) } +// We need to inject the row's `Objects` to work-around improper pluralization in UnsafeGuessKindToResource +func GatewayFactory(ctor func(context.Context, *Listers, configmap.Watcher, *TableRow) controller.Reconciler) Factory { + return func(t *testing.T, r *TableRow) ( + controller.Reconciler, ActionRecorderList, EventList, + ) { + shim := func(c context.Context, l *Listers, cw configmap.Watcher) controller.Reconciler { + return ctor(c, l, cw, r) + } + return MakeFactory(shim)(t, r) + } +} + +type GatewayOption func(*gatewayv1alpha2.Gateway) + +func gw(opts ...GatewayOption) *gatewayv1alpha2.Gateway { + g := &gatewayv1alpha2.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: publicName, + Namespace: testNamespace, + }, + Spec: gatewayv1alpha2.GatewaySpec{ + GatewayClassName: gatewayAPIIngressClassName, + }, + } + for _, opt := range opts { + opt(g) + } + return g +} + +func defaultListener(g *gatewayv1alpha2.Gateway) { + g.Spec.Listeners = append(g.Spec.Listeners, gatewayv1alpha2.Listener{ + Name: "http", + Port: 80, + Protocol: "HTTP", + }) +} + +func tlsListener(hostname, nsName, secretName string) GatewayOption { + return func(g *gatewayv1alpha2.Gateway) { + g.Spec.Listeners = append(g.Spec.Listeners, gatewayv1alpha2.Listener{ + Name: gatewayv1alpha2.SectionName("kni-"), + Hostname: (*gatewayv1alpha2.Hostname)(&hostname), + Port: 443, + Protocol: "HTTPS", + TLS: &gatewayv1alpha2.GatewayTLSConfig{ + Mode: (*gatewayv1alpha2.TLSModeType)(pointer.String("Terminate")), + CertificateRefs: []gatewayv1alpha2.SecretObjectReference{{ + Group: (*gatewayv1alpha2.Group)(pointer.String("")), + Kind: (*gatewayv1alpha2.Kind)(pointer.String("Secret")), + Name: gatewayv1alpha2.ObjectName(secretName), + Namespace: (*gatewayv1alpha2.Namespace)(&nsName), + }}, + Options: map[gatewayv1alpha2.AnnotationKey]gatewayv1alpha2.AnnotationValue{}, + }, + AllowedRoutes: &gatewayv1alpha2.AllowedRoutes{ + Namespaces: &gatewayv1alpha2.RouteNamespaces{ + From: (*gatewayv1alpha2.FromNamespaces)(pointer.String("Selector")), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "kubernetes.io/metadata.name": nsName, + }, + }, + }, + Kinds: []gatewayv1alpha2.RouteGroupKind{}, + }, + }) + } +} + +var withFinalizer = func(i *v1alpha1.Ingress) { + i.Finalizers = append(i.Finalizers, "ingresses.networking.internal.knative.dev") +} + +func withTLS(secret string) IngressOption { + return func(i *v1alpha1.Ingress) { + i.Spec.TLS = append(i.Spec.TLS, v1alpha1.IngressTLS{ + Hosts: []string{"secure.example.com"}, + SecretName: "name-WE-STICK-A-LONG-UID-HERE", + SecretNamespace: "ns", + }) + } +} + +func secret(name, ns string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + }, + StringData: map[string]string{ + "ca.crt": "signed thing", + "ca.key": "private thing", + }, + Type: "kubernetes.io/tls", + } +} + +func rp(to *corev1.Secret) *gatewayv1alpha2.ReferencePolicy { + t := true + return &gatewayv1alpha2.ReferencePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: to.Name + "-" + testNamespace, + Namespace: to.Namespace, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: "networking.internal.knative.dev/v1alpha1", + Kind: "Ingress", + Name: "name", + Controller: &t, + BlockOwnerDeletion: &t, + }}, + }, + Spec: gatewayv1alpha2.ReferenceGrantSpec{ + From: []gatewayv1alpha2.ReferenceGrantFrom{{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Namespace: gatewayv1alpha2.Namespace(testNamespace), + }}, + To: []gatewayv1alpha2.ReferenceGrantTo{{ + Group: gatewayv1alpha2.Group(""), + Kind: gatewayv1alpha2.Kind("Secret"), + Name: (*gatewayv1alpha2.ObjectName)(&to.Name), + }}, + }, + } +} + var ( defaultConfig = &config.Config{ Network: &networkcfg.Config{}, diff --git a/pkg/reconciler/ingress/reconcile_resources.go b/pkg/reconciler/ingress/reconcile_resources.go index 0c152843d..46409ec63 100644 --- a/pkg/reconciler/ingress/reconcile_resources.go +++ b/pkg/reconciler/ingress/reconcile_resources.go @@ -24,18 +24,23 @@ import ( "k8s.io/apimachinery/pkg/api/equality" apierrs "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - gatewayv1alpa2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "knative.dev/net-gateway-api/pkg/reconciler/ingress/config" "knative.dev/net-gateway-api/pkg/reconciler/ingress/resources" netv1alpha1 "knative.dev/networking/pkg/apis/networking/v1alpha1" "knative.dev/pkg/controller" ) +const listenerPrefix = "kni-" + // reconcileHTTPRoute reconciles HTTPRoute. func (c *Reconciler) reconcileHTTPRoute( ctx context.Context, ing *netv1alpha1.Ingress, rule *netv1alpha1.IngressRule, -) (*gatewayv1alpa2.HTTPRoute, error) { +) (*gatewayv1alpha2.HTTPRoute, error) { recorder := controller.GetEventRecorder(ctx) httproute, err := c.httprouteLister.HTTPRoutes(ing.Namespace).Get(resources.LongestHost(rule.Hosts)) @@ -82,3 +87,195 @@ func (c *Reconciler) reconcileHTTPRoute( return httproute, err } + +func (c *Reconciler) reconcileTLS( + ctx context.Context, tls *netv1alpha1.IngressTLS, ing *netv1alpha1.Ingress, +) ( + []*gatewayv1alpha2.Listener, error) { + recorder := controller.GetEventRecorder(ctx) + gatewayConfig := config.FromContext(ctx).Gateway.Gateways + externalGw := gatewayConfig[netv1alpha1.IngressVisibilityExternalIP] + + gateway := metav1.PartialObjectMetadata{ + TypeMeta: metav1.TypeMeta{ + Kind: "Gateway", + APIVersion: gatewayv1alpha2.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: externalGw.Gateway.Name, + Namespace: externalGw.Gateway.Namespace, + }, + } + secret := metav1.PartialObjectMetadata{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: corev1.SchemeGroupVersion.Version, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: tls.SecretName, + Namespace: tls.SecretNamespace, + }, + } + + desired := resources.MakeReferenceGrant(ctx, ing, secret, gateway) + + rp, err := c.referencePolicyLister.ReferencePolicies(desired.Namespace).Get(desired.Name) + + if apierrs.IsNotFound(err) { + rp, err = c.gwapiclient.GatewayV1alpha2().ReferencePolicies(desired.Namespace).Create(ctx, desired, metav1.CreateOptions{}) + + if err != nil { + recorder.Eventf(ing, corev1.EventTypeWarning, "CreationFailed", "Failed to create ReferencePolicy: %v", err) + return nil, fmt.Errorf("failed to create ReferencePolicy: %w", err) + } + } else if err != nil { + return nil, err + } + + if !metav1.IsControlledBy(rp, ing) { + recorder.Eventf(ing, corev1.EventTypeWarning, "NotOwned", "ReferencePolicy %s not owned by this object", desired.Name) + return nil, fmt.Errorf("ReferencePolicy %s not owned by %s", rp.Name, ing.Name) + } + + if !equality.Semantic.DeepEqual(rp.Spec, desired.Spec) { + update := rp.DeepCopy() + update.Spec = desired.Spec + + _, err := c.gwapiclient.GatewayV1alpha2().ReferencePolicies(update.Namespace).Update(ctx, update, metav1.UpdateOptions{}) + if err != nil { + recorder.Eventf(ing, corev1.EventTypeWarning, "UpdateFailed", "Failed to update ReferencePolicy: %v", err) + return nil, fmt.Errorf("failed to update ReferencePolicy: %w", err) + } + } + + // Gateway API loves typed pointers and constants, so we need to copy the constants + // to something we can reference + mode := gatewayv1alpha2.TLSModeTerminate + selector := gatewayv1alpha2.NamespacesFromSelector + listeners := make([]*gatewayv1alpha2.Listener, 0, len(tls.Hosts)) + for _, h := range tls.Hosts { + h := h + listener := gatewayv1alpha2.Listener{ + Name: gatewayv1alpha2.SectionName(listenerPrefix + ing.GetUID()), + Hostname: (*gatewayv1alpha2.Hostname)(&h), + Port: 443, + Protocol: gatewayv1alpha2.HTTPSProtocolType, + TLS: &gatewayv1alpha2.GatewayTLSConfig{ + Mode: &mode, + CertificateRefs: []gatewayv1alpha2.SecretObjectReference{{ + Group: (*gatewayv1alpha2.Group)(pointer.String("")), + Kind: (*gatewayv1alpha2.Kind)(pointer.String("Secret")), + Name: gatewayv1alpha2.ObjectName(tls.SecretName), + Namespace: (*gatewayv1alpha2.Namespace)(&tls.SecretNamespace), + }}, + }, + AllowedRoutes: &gatewayv1alpha2.AllowedRoutes{ + Namespaces: &gatewayv1alpha2.RouteNamespaces{ + From: &selector, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + corev1.LabelMetadataName: ing.Namespace, + }, + }, + }, + Kinds: []gatewayv1alpha2.RouteGroupKind{}, + }, + } + listeners = append(listeners, &listener) + } + + return listeners, err +} + +func (c *Reconciler) reconcileGatewayListeners( + ctx context.Context, listeners []*gatewayv1alpha2.Listener, + ing *netv1alpha1.Ingress, gwName types.NamespacedName, +) error { + recorder := controller.GetEventRecorder(ctx) + gw, err := c.gatewayLister.Gateways(gwName.Namespace).Get(gwName.Name) + if apierrs.IsNotFound(err) { + recorder.Eventf(ing, corev1.EventTypeWarning, "GatewayMissing", "Unable to update Gateway %s", gwName.String()) + return fmt.Errorf("Gateway %s does not exist: %w", gwName, err) //nolint:stylecheck + } else if err != nil { + return err + } + + update := gw.DeepCopy() + + lmap := map[string]*gatewayv1alpha2.Listener{} + for _, l := range listeners { + lmap[string(l.Name)] = l + } + // TODO: how do we track and remove listeners if they are removed from the KIngress spec? + // Tracked in https://github.com/knative-sandbox/net-gateway-api/issues/319 + + updated := false + for i, l := range gw.Spec.Listeners { + l := l + desired, ok := lmap[string(l.Name)] + if !ok { + // This listener doesn't match any that we control. + continue + } + delete(lmap, string(l.Name)) + if equality.Semantic.DeepEqual(&l, desired) { + // Already present and correct + continue + } + update.Spec.Listeners[i] = *desired + updated = true + } + + for _, l := range lmap { + // Add all remaining listeners + update.Spec.Listeners = append(update.Spec.Listeners, *l) + updated = true + } + + if updated { + _, err := c.gwapiclient.GatewayV1alpha2().Gateways(update.Namespace).Update( + ctx, update, metav1.UpdateOptions{}) + if err != nil { + recorder.Eventf(ing, corev1.EventTypeWarning, "GatewayUpdateFailed", "Failed to update Gateway %s: %v", gwName, err) + return fmt.Errorf("failed to update Gateway %s/%s: %w", update.Namespace, update.Name, err) + } + } + + return nil +} + +func (c *Reconciler) clearGatewayListeners(ctx context.Context, ing *netv1alpha1.Ingress, gwName *types.NamespacedName) error { + recorder := controller.GetEventRecorder(ctx) + + gw, err := c.gatewayLister.Gateways(gwName.Namespace).Get(gwName.Name) + if apierrs.IsNotFound(err) { + // Nothing to clean up, all done! + return nil + } else if err != nil { + return err + } + + listenerName := listenerPrefix + string(ing.GetUID()) + update := gw.DeepCopy() + + numListeners := len(update.Spec.Listeners) + for i := numListeners - 1; i >= 0; i-- { + // March backwards down the list removing items by swapping in the last item and trimming the list + // A generic list.remove(func) would be nice here. + l := update.Spec.Listeners[i] + if string(l.Name) == listenerName { + update.Spec.Listeners[i] = update.Spec.Listeners[len(update.Spec.Listeners)-1] + update.Spec.Listeners = update.Spec.Listeners[:len(update.Spec.Listeners)-1] + } + } + + if len(update.Spec.Listeners) != numListeners { + _, err := c.gwapiclient.GatewayV1alpha2().Gateways(update.Namespace).Update(ctx, update, metav1.UpdateOptions{}) + if err != nil { + recorder.Eventf(ing, corev1.EventTypeWarning, "GatewayUpdateFailed", "Failed to remove Listener from Gateway %s: %v", gwName, err) + return fmt.Errorf("failed to update Gateway %s/%s: %w", update.Namespace, update.Name, err) + } + } + + return nil +} diff --git a/pkg/reconciler/ingress/resources/httproute.go b/pkg/reconciler/ingress/resources/httproute.go index 7e6db8514..d0847c035 100644 --- a/pkg/reconciler/ingress/resources/httproute.go +++ b/pkg/reconciler/ingress/resources/httproute.go @@ -75,6 +75,8 @@ func makeHTTPRouteSpec( namespacedNameGateway := gatewayConfig.Gateways[rule.Visibility].Gateway gatewayRef := gatewayv1alpha2.ParentReference{ + Group: (*gatewayv1alpha2.Group)(&gatewayv1alpha2.GroupVersion.Group), + Kind: (*gatewayv1alpha2.Kind)(pointer.String("Gateway")), Namespace: namespacePtr(gatewayv1alpha2.Namespace(namespacedNameGateway.Namespace)), Name: gatewayv1alpha2.ObjectName(namespacedNameGateway.Name), } @@ -132,8 +134,10 @@ func makeHTTPRouteRule(rule *netv1alpha1.IngressRule) []gatewayv1alpha2.HTTPRout backendRef := gatewayv1alpha2.HTTPBackendRef{ BackendRef: gatewayv1alpha2.BackendRef{ BackendObjectReference: gatewayv1alpha2.BackendObjectReference{ - Port: portNumPtr(split.ServicePort.IntValue()), - Name: gatewayv1alpha2.ObjectName(name), + Group: (*gatewayv1alpha2.Group)(pointer.String("")), + Kind: (*gatewayv1alpha2.Kind)(pointer.String("Service")), + Port: portNumPtr(split.ServicePort.IntValue()), + Name: gatewayv1alpha2.ObjectName(name), }, Weight: pointer.Int32Ptr(int32(split.Percent)), }, @@ -155,7 +159,7 @@ func makeHTTPRouteRule(rule *netv1alpha1.IngressRule) []gatewayv1alpha2.HTTPRout Value: pointer.StringPtr(pathPrefix), } - headerMatchList := []gatewayv1alpha2.HTTPHeaderMatch{} + var headerMatchList []gatewayv1alpha2.HTTPHeaderMatch for k, v := range path.Headers { headerMatch := gatewayv1alpha2.HTTPHeaderMatch{ Type: headerMatchTypePtr(gatewayv1alpha2.HeaderMatchExact), diff --git a/pkg/reconciler/ingress/resources/httproute_test.go b/pkg/reconciler/ingress/resources/httproute_test.go index a6b2d9af1..6e2116881 100644 --- a/pkg/reconciler/ingress/resources/httproute_test.go +++ b/pkg/reconciler/ingress/resources/httproute_test.go @@ -152,8 +152,10 @@ func TestMakeHTTPRoute(t *testing.T) { BackendRefs: []gatewayv1alpha2.HTTPBackendRef{{ BackendRef: gatewayv1alpha2.BackendRef{ BackendObjectReference: gatewayv1alpha2.BackendObjectReference{ - Port: portNumPtr(123), - Name: gatewayv1alpha2.ObjectName("goo"), + Group: (*gatewayv1alpha2.Group)(pointer.String("")), + Kind: (*gatewayv1alpha2.Kind)(pointer.String("Service")), + Name: gatewayv1alpha2.ObjectName("goo"), + Port: portNumPtr(123), }, Weight: pointer.Int32Ptr(int32(12)), }, @@ -173,8 +175,10 @@ func TestMakeHTTPRoute(t *testing.T) { }, { BackendRef: gatewayv1alpha2.BackendRef{ BackendObjectReference: gatewayv1alpha2.BackendObjectReference{ - Port: portNumPtr(124), - Name: gatewayv1alpha2.ObjectName("doo"), + Group: (*gatewayv1alpha2.Group)(pointer.String("")), + Kind: (*gatewayv1alpha2.Kind)(pointer.String("Service")), + Port: portNumPtr(124), + Name: gatewayv1alpha2.ObjectName("doo"), }, Weight: pointer.Int32Ptr(int32(88)), }, @@ -205,12 +209,13 @@ func TestMakeHTTPRoute(t *testing.T) { Type: pathMatchTypePtr(gatewayv1alpha2.PathMatchPathPrefix), Value: pointer.StringPtr("/"), }, - Headers: []gatewayv1alpha2.HTTPHeaderMatch{}, }, }, }}, CommonRouteSpec: gatewayv1alpha2.CommonRouteSpec{ ParentRefs: []gatewayv1alpha2.ParentReference{{ + Group: (*gatewayv1alpha2.Group)(pointer.String("gateway.networking.k8s.io")), + Kind: (*gatewayv1alpha2.Kind)(pointer.String("Gateway")), Namespace: namespacePtr("test-ns"), Name: gatewayv1alpha2.ObjectName("foo"), }}, @@ -232,8 +237,10 @@ func TestMakeHTTPRoute(t *testing.T) { BackendRefs: []gatewayv1alpha2.HTTPBackendRef{{ BackendRef: gatewayv1alpha2.BackendRef{ BackendObjectReference: gatewayv1alpha2.BackendObjectReference{ - Port: portNumPtr(123), - Name: gatewayv1alpha2.ObjectName("goo"), + Group: (*gatewayv1alpha2.Group)(pointer.String("")), + Kind: (*gatewayv1alpha2.Kind)(pointer.String("Service")), + Port: portNumPtr(123), + Name: gatewayv1alpha2.ObjectName("goo"), }, Weight: pointer.Int32Ptr(int32(12)), }, @@ -253,8 +260,10 @@ func TestMakeHTTPRoute(t *testing.T) { }, { BackendRef: gatewayv1alpha2.BackendRef{ BackendObjectReference: gatewayv1alpha2.BackendObjectReference{ - Port: portNumPtr(124), - Name: gatewayv1alpha2.ObjectName("doo"), + Group: (*gatewayv1alpha2.Group)(pointer.String("")), + Kind: (*gatewayv1alpha2.Kind)(pointer.String("Service")), + Port: portNumPtr(124), + Name: gatewayv1alpha2.ObjectName("doo"), }, Weight: pointer.Int32Ptr(int32(88)), }, @@ -284,11 +293,12 @@ func TestMakeHTTPRoute(t *testing.T) { Type: pathMatchTypePtr(gatewayv1alpha2.PathMatchPathPrefix), Value: pointer.StringPtr("/"), }, - Headers: []gatewayv1alpha2.HTTPHeaderMatch{}, }}, }}, CommonRouteSpec: gatewayv1alpha2.CommonRouteSpec{ ParentRefs: []gatewayv1alpha2.ParentReference{{ + Group: (*gatewayv1alpha2.Group)(pointer.String("gateway.networking.k8s.io")), + Kind: (*gatewayv1alpha2.Kind)(pointer.String("Gateway")), Namespace: namespacePtr("test-ns"), Name: gatewayv1alpha2.ObjectName("foo-local"), }}, @@ -358,8 +368,10 @@ func TestMakeHTTPRoute(t *testing.T) { BackendRefs: []gatewayv1alpha2.HTTPBackendRef{{ BackendRef: gatewayv1alpha2.BackendRef{ BackendObjectReference: gatewayv1alpha2.BackendObjectReference{ - Port: portNumPtr(123), - Name: gatewayv1alpha2.ObjectName("goo"), + Group: (*gatewayv1alpha2.Group)(pointer.String("")), + Kind: (*gatewayv1alpha2.Kind)(pointer.String("Service")), + Port: portNumPtr(123), + Name: gatewayv1alpha2.ObjectName("goo"), }, Weight: pointer.Int32Ptr(int32(100)), }, @@ -384,8 +396,10 @@ func TestMakeHTTPRoute(t *testing.T) { BackendRefs: []gatewayv1alpha2.HTTPBackendRef{{ BackendRef: gatewayv1alpha2.BackendRef{ BackendObjectReference: gatewayv1alpha2.BackendObjectReference{ - Port: portNumPtr(124), - Name: gatewayv1alpha2.ObjectName("doo"), + Group: (*gatewayv1alpha2.Group)(pointer.String("")), + Kind: (*gatewayv1alpha2.Kind)(pointer.String("Service")), + Port: portNumPtr(124), + Name: gatewayv1alpha2.ObjectName("doo"), }, Weight: pointer.Int32Ptr(int32(100)), }, @@ -410,6 +424,8 @@ func TestMakeHTTPRoute(t *testing.T) { }, CommonRouteSpec: gatewayv1alpha2.CommonRouteSpec{ ParentRefs: []gatewayv1alpha2.ParentReference{{ + Group: (*gatewayv1alpha2.Group)(pointer.String("gateway.networking.k8s.io")), + Kind: (*gatewayv1alpha2.Kind)(pointer.String("Gateway")), Namespace: namespacePtr("test-ns"), Name: gatewayv1alpha2.ObjectName("foo"), }}, diff --git a/pkg/reconciler/ingress/resources/reference_grant.go b/pkg/reconciler/ingress/resources/reference_grant.go new file mode 100644 index 000000000..c23ab7ffd --- /dev/null +++ b/pkg/reconciler/ingress/resources/reference_grant.go @@ -0,0 +1,62 @@ +/* +Copyright 2021 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + netv1alpha1 "knative.dev/networking/pkg/apis/networking/v1alpha1" + "knative.dev/pkg/kmeta" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +// Grant the resource "to" access to the resource "from" +// TODO: remove ReferencePolicy return value once Istio supports ReferenceGrant +func MakeReferenceGrant(ctx context.Context, ing *netv1alpha1.Ingress, to, from metav1.PartialObjectMetadata) *gatewayv1alpha2.ReferencePolicy { + name := to.Name + if len(name)+len(from.Namespace) > 62 { + name = name[:62-len(from.Namespace)] + } + name += "-" + from.Namespace + + spec := gatewayv1alpha2.ReferenceGrantSpec{ + From: []gatewayv1alpha2.ReferenceGrantFrom{{ + Group: gatewayv1alpha2.Group(from.GroupVersionKind().Group), + Kind: gatewayv1alpha2.Kind(from.Kind), + Namespace: gatewayv1alpha2.Namespace(from.Namespace), + }}, + To: []gatewayv1alpha2.ReferenceGrantTo{{ + Group: gatewayv1alpha2.Group(to.GroupVersionKind().Group), + Kind: gatewayv1alpha2.Kind(to.Kind), + Name: (*gatewayv1alpha2.ObjectName)(&to.Name), + }}, + } + + legacy := &gatewayv1alpha2.ReferencePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: to.Namespace, + Labels: to.Labels, + Annotations: to.Annotations, + OwnerReferences: []metav1.OwnerReference{*kmeta.NewControllerRef(ing)}, + }, + Spec: spec, + } + + return legacy +} diff --git a/pkg/reconciler/testing/listers.go b/pkg/reconciler/testing/listers.go index 5c799c6c4..bfe963f12 100644 --- a/pkg/reconciler/testing/listers.go +++ b/pkg/reconciler/testing/listers.go @@ -99,3 +99,11 @@ func (l *Listers) GetHTTPRouteLister() gatewayListers.HTTPRouteLister { func (l *Listers) GetEndpointsLister() corev1listers.EndpointsLister { return corev1listers.NewEndpointsLister(l.IndexerFor(&corev1.Endpoints{})) } + +func (l *Listers) GetGatewayLister() gatewayListers.GatewayLister { + return gatewayListers.NewGatewayLister(l.IndexerFor(&gatewayv1alpa2.Gateway{})) +} + +func (l *Listers) GetReferencePolicyLister() gatewayListers.ReferencePolicyLister { + return gatewayListers.NewReferencePolicyLister(l.IndexerFor(&gatewayv1alpa2.ReferencePolicy{})) +}