diff --git a/pkg/controller/gatewayapi/gatewayapi_controller.go b/pkg/controller/gatewayapi/gatewayapi_controller.go index d6744dc288..1aa4f8eaf3 100644 --- a/pkg/controller/gatewayapi/gatewayapi_controller.go +++ b/pkg/controller/gatewayapi/gatewayapi_controller.go @@ -262,6 +262,19 @@ func (r *ReconcileGatewayAPI) Reconcile(ctx context.Context, request reconcile.R reqLogger.Info("Could not render all optional GatewayAPI CRDs", "err", err) } + // Render the tigera-gateway Namespace early — before any of the steps below that may + // early-return (e.g. EnvoyProxyRef resolution). The namespace is part of the operator's + // contract for any GatewayAPI CR, so it must always exist; otherwise users referencing + // custom EnvoyProxy resources in tigera-gateway on a fresh install hit a deadlock where + // reconcile fails on the missing EnvoyProxy and never reaches the non-CRD render that + // would have created the namespace. + namespaceComponent := gatewayapi.GatewayAPINamespaceComponent(installationSpec) + err = r.newComponentHandler(log, r.client, r.scheme, gatewayAPI).CreateOrUpdateOrDelete(ctx, namespaceComponent, nil) + if err != nil { + r.status.SetDegraded(operatorv1.ResourceCreateError, "Error rendering tigera-gateway namespace", err, log) + return reconcile.Result{}, err + } + pullSecrets, err := utils.GetInstallationPullSecrets(installationSpec, r.client) if err != nil { r.status.SetDegraded(operatorv1.ResourceReadError, "Error retrieving pull secrets", err, reqLogger) diff --git a/pkg/controller/gatewayapi/gatewayapi_controller_test.go b/pkg/controller/gatewayapi/gatewayapi_controller_test.go index 90437ac610..ca3ebc16bc 100644 --- a/pkg/controller/gatewayapi/gatewayapi_controller_test.go +++ b/pkg/controller/gatewayapi/gatewayapi_controller_test.go @@ -225,12 +225,13 @@ var _ = Describe("Gateway API controller tests", func() { Expect(err).ShouldNot(HaveOccurred()) By("checking the component handlers") - Expect(fakeComponentHandlers).To(HaveLen(2)) + Expect(fakeComponentHandlers).To(HaveLen(3)) Expect(fakeComponentHandlers[0].createOnly).To(BeTrue()) Expect(fakeComponentHandlers[1].createOnly).To(BeFalse()) + Expect(fakeComponentHandlers[2].createOnly).To(BeFalse()) By("checking that the custom EnvoyGateway was passed through") - gatewayAPIImplementationConfig := fakeComponentHandlers[1].lastComponent.(gatewayapi.GatewayAPIImplementationConfigInterface).GetConfig() + gatewayAPIImplementationConfig := fakeComponentHandlers[2].lastComponent.(gatewayapi.GatewayAPIImplementationConfigInterface).GetConfig() Expect(gatewayAPIImplementationConfig.CustomEnvoyGateway).NotTo(BeNil()) Expect(*gatewayAPIImplementationConfig.CustomEnvoyGateway).To(Equal(*envoyGateway)) }) @@ -300,7 +301,7 @@ var _ = Describe("Gateway API controller tests", func() { Expect(err).ShouldNot(HaveOccurred()) By("checking that the custom EnvoyGateway was passed through") - gatewayAPIImplementationConfig := fakeComponentHandlers[1].lastComponent.(gatewayapi.GatewayAPIImplementationConfigInterface).GetConfig() + gatewayAPIImplementationConfig := fakeComponentHandlers[2].lastComponent.(gatewayapi.GatewayAPIImplementationConfigInterface).GetConfig() Expect(gatewayAPIImplementationConfig.CustomEnvoyGateway).NotTo(BeNil()) Expect(*gatewayAPIImplementationConfig.CustomEnvoyGateway).To(Equal(*envoyGateway)) }) @@ -467,12 +468,13 @@ var _ = Describe("Gateway API controller tests", func() { Expect(err).ShouldNot(HaveOccurred()) By("checking the component handlers") - Expect(fakeComponentHandlers).To(HaveLen(2)) + Expect(fakeComponentHandlers).To(HaveLen(3)) Expect(fakeComponentHandlers[0].createOnly).To(BeTrue()) Expect(fakeComponentHandlers[1].createOnly).To(BeFalse()) + Expect(fakeComponentHandlers[2].createOnly).To(BeFalse()) By("checking that the custom EnvoyProxies were passed through") - gatewayAPIImplementationConfig := fakeComponentHandlers[1].lastComponent.(gatewayapi.GatewayAPIImplementationConfigInterface).GetConfig() + gatewayAPIImplementationConfig := fakeComponentHandlers[2].lastComponent.(gatewayapi.GatewayAPIImplementationConfigInterface).GetConfig() Expect(gatewayAPIImplementationConfig.CustomEnvoyProxies).NotTo(BeNil()) Expect(gatewayAPIImplementationConfig.CustomEnvoyProxies).To(HaveKeyWithValue("custom-class-1", envoyProxy1)) Expect(gatewayAPIImplementationConfig.CustomEnvoyProxies).To(HaveKeyWithValue("custom-class-2", envoyProxy2)) @@ -534,6 +536,56 @@ var _ = Describe("Gateway API controller tests", func() { ).Return() _, err := r.Reconcile(ctx, reconcile.Request{}) Expect(err).Should(HaveOccurred()) + + By("checking the tigera-gateway namespace was still rendered before the EnvoyProxyRef failure") + // Regression: previously, an EnvoyProxyRef pointing at a not-yet-existent EnvoyProxy in + // tigera-gateway caused reconcile to early-return before the namespace was created, + // deadlocking the user (they couldn't create the EnvoyProxy because the namespace + // didn't exist). The namespace must now be rendered before EnvoyProxyRef resolution. + Expect(fakeComponentHandlers).To(HaveLen(2)) + Expect(fakeComponentHandlers[0].createOnly).To(BeTrue()) + Expect(fakeComponentHandlers[1].createOnly).To(BeFalse()) + nsObjs, _ := fakeComponentHandlers[1].lastComponent.Objects() + Expect(nsObjs).To(HaveLen(1)) + Expect(nsObjs[0]).To(BeAssignableToTypeOf(&corev1.Namespace{})) + Expect(nsObjs[0].GetName()).To(Equal("tigera-gateway")) + }) + + It("renders the tigera-gateway namespace when only an EnvoyProxyRef is configured and the EnvoyProxy is missing", func() { + Expect(c.Create(ctx, installation)).NotTo(HaveOccurred()) + + By("applying a GatewayAPI CR with an EnvoyProxyRef but no matching EnvoyProxy") + gwapi := &operatorv1.GatewayAPI{ + ObjectMeta: metav1.ObjectMeta{Name: "tigera-secure"}, + Spec: operatorv1.GatewayAPISpec{ + GatewayClasses: []operatorv1.GatewayClassSpec{{ + Name: "custom-class-1", + EnvoyProxyRef: &operatorv1.NamespacedName{ + Namespace: "tigera-gateway", + Name: "missing-proxy", + }, + }}, + }, + } + Expect(c.Create(ctx, gwapi)).NotTo(HaveOccurred()) + + By("triggering a reconcile") + mockStatus.On( + "SetDegraded", + operatorv1.ResourceReadError, + "Error reading EnvoyProxyRef", + "envoyproxies.gateway.envoyproxy.io \"missing-proxy\" not found", + mock.Anything, + ).Return() + _, err := r.Reconcile(ctx, reconcile.Request{}) + Expect(err).Should(HaveOccurred()) + + By("verifying the namespace component still ran (CRDs + namespace, no nonCRD impl)") + Expect(fakeComponentHandlers).To(HaveLen(2)) + nsObjs, _ := fakeComponentHandlers[1].lastComponent.Objects() + Expect(nsObjs).To(HaveLen(1)) + Expect(nsObjs[0]).To(BeAssignableToTypeOf(&corev1.Namespace{})) + Expect(nsObjs[0].GetName()).To(Equal("tigera-gateway")) }) It("handles when both GatewayKind and an incompatible EnvoyProxy are specified", func() { diff --git a/pkg/render/gatewayapi/gateway_api.go b/pkg/render/gatewayapi/gateway_api.go index e64cbe6f95..f28bcafe4d 100644 --- a/pkg/render/gatewayapi/gateway_api.go +++ b/pkg/render/gatewayapi/gateway_api.go @@ -424,6 +424,34 @@ func GatewayAPIImplementationComponent(cfg *GatewayAPIImplementationConfig) rend } } +// GatewayAPINamespaceComponent emits only the tigera-gateway Namespace. The controller renders +// this early — alongside CRDs and before EnvoyProxyRef resolution — so that users can create +// EnvoyProxy resources in tigera-gateway on a fresh install without the controller deadlocking +// on a missing namespace. +func GatewayAPINamespaceComponent(installation *operatorv1.InstallationSpec) render.Component { + return &gatewayAPINamespaceComponent{installation: installation} +} + +type gatewayAPINamespaceComponent struct { + installation *operatorv1.InstallationSpec +} + +func (c *gatewayAPINamespaceComponent) ResolveImages(*operatorv1.ImageSet) error { return nil } +func (c *gatewayAPINamespaceComponent) SupportedOSType() rmeta.OSType { return rmeta.OSTypeLinux } +func (c *gatewayAPINamespaceComponent) Ready() bool { return true } + +func (c *gatewayAPINamespaceComponent) Objects() ([]client.Object, []client.Object) { + resources := GatewayAPIResources() + return []client.Object{ + render.CreateNamespace( + resources.namespace.Name, + c.installation.KubernetesProvider, + render.PSSPrivileged, // Needed for HostPath volume to write logs to + c.installation.Azure, + ), + }, nil +} + func (pr *gatewayAPIImplementationComponent) ResolveImages(is *operatorv1.ImageSet) error { reg := pr.cfg.Installation.Registry path := pr.cfg.Installation.ImagePath