diff --git a/pkg/controller/installation/core_controller.go b/pkg/controller/installation/core_controller.go index c4dcdd6aaa..3883eb9a20 100644 --- a/pkg/controller/installation/core_controller.go +++ b/pkg/controller/installation/core_controller.go @@ -1381,7 +1381,15 @@ func (r *ReconcileInstallation) Reconcile(ctx context.Context, request reconcile // spec.extensions.waf.state != Enabled, the WAF surface is not rendered. // See design tigera/designs#25 (PMREQ-384) §Gating. wafGatewayExtensionEnabled := false + // gatewayAPIPresent means the GatewayAPI CR exists (regardless of waf.state), + // so the operator manages the Gateway API + Envoy Gateway CRDs the WAF + // reconcilers watch. It keeps the applicationlayer controller wired (with + // EnvoyExtensionPolicy delete RBAC) even while WAF is disabled, so the + // controller can tear down the EEPs it generated instead of being removed in + // the same reconcile that disables WAF (EV-6751). + gatewayAPIPresent := false if gatewayAPI, msg, err := gatewayapi.GetGatewayAPI(ctx, r.client); err == nil { + gatewayAPIPresent = true wafGatewayExtensionEnabled = gatewayAPI.Spec.IsWAFGatewayExtensionEnabled() } else if !apierrors.IsNotFound(err) { // Mirrors the GatewayAPI controller's handling: a read error or a @@ -1698,6 +1706,7 @@ func (r *ReconcileInstallation) Reconcile(ctx context.Context, request reconcile Namespace: common.CalicoNamespace, BindingNamespaces: []string{common.CalicoNamespace}, WAFGatewayExtensionEnabled: wafGatewayExtensionEnabled, + GatewayAPIPresent: gatewayAPIPresent, WAFWebhookServerTLS: wafWebhookTLS, WASMPullSecret: wasmPullSecret, WASMCACert: wasmCACert, diff --git a/pkg/render/kubecontrollers/kube-controllers.go b/pkg/render/kubecontrollers/kube-controllers.go index ad3c9f93cf..674d1b7002 100644 --- a/pkg/render/kubecontrollers/kube-controllers.go +++ b/pkg/render/kubecontrollers/kube-controllers.go @@ -128,15 +128,26 @@ type KubeControllersConfiguration struct { // If this is nil, then we should run in zero-tenant mode. Tenant *operatorv1.Tenant - // WAFGatewayExtensionEnabled gates the WAF v3 (Gateway API add-on) surface - // on calico-kube-controllers: the applicationlayer controller enablement, - // the WAF / Gateway-API / EnvoyExtensionPolicy / event / secret-replication - // RBAC, the WASM_IMAGE / WASM_PULL_SECRET / WASM_CA_CERT env vars, and the - // gateway envoy-proxy wasm image resolution. Sourced from + // WAFGatewayExtensionEnabled gates the ACTIVE WAF v3 (Gateway API add-on) + // surface on calico-kube-controllers: the WASM_IMAGE / WASM_PULL_SECRET / + // WASM_CA_CERT env vars, the in-process admission webhook, and the gateway + // envoy-proxy wasm image resolution. Sourced from // `GatewayAPI.spec.extensions.waf.state == Enabled` (default off). // See design `tigera/designs#25` (PMREQ-384). WAFGatewayExtensionEnabled bool + // GatewayAPIPresent is true when the GatewayAPI CR exists (regardless of + // waf.state), so the operator manages the Gateway API + Envoy Gateway CRDs the + // WAF reconcilers watch. It gates the applicationlayer controller enablement, + // its WAF / Gateway-API / EnvoyExtensionPolicy / event / secret RBAC, and the + // WAF_GATEWAY_EXTENSION_ENABLED signal env — a superset of + // WAFGatewayExtensionEnabled. The WAF controller stays wired (and keeps its + // envoyextensionpolicies delete RBAC) while WAF is disabled precisely so it can + // tear down the EnvoyExtensionPolicies it generated, instead of being removed + // in the same reconcile that disables WAF and never getting the chance + // (EV-6751). It de-programs on WAF_GATEWAY_EXTENSION_ENABLED=false. + GatewayAPIPresent bool + // WAFWebhookServerTLS is the serving certificate for the in-process WAF // SecLang validating admission webhook hosted by calico-kube-controllers. // When set (WAF enabled), it is mounted into the Pod and the webhook server @@ -197,7 +208,11 @@ func NewCalicoKubeControllers(cfg *KubeControllersConfiguration) *kubeController }, ) enabledControllers = append(enabledControllers, "service", "federatedservices", "usage") - if cfg.WAFGatewayExtensionEnabled { + // Wire the applicationlayer WAF controller whenever Gateway API is present, + // not only when WAF is enabled, so it stays running (and can tear down its + // generated EnvoyExtensionPolicies) when WAF is disabled. It de-programs vs + // programs based on WAF_GATEWAY_EXTENSION_ENABLED (EV-6751). + if cfg.GatewayAPIPresent { enabledControllers = append(enabledControllers, "applicationlayer") } } @@ -560,9 +575,12 @@ func kubeControllersRoleEnterpriseCommonRules(cfg *KubeControllersConfiguration) }, } - if cfg.WAFGatewayExtensionEnabled { - // WAF v3 (Gateway API add-on) RBAC. Gated by - // GatewayAPI.spec.extensions.waf.state == Enabled. + if cfg.GatewayAPIPresent { + // WAF v3 (Gateway API add-on) RBAC. Gated by GatewayAPIPresent, not + // waf.state==Enabled, so the applicationlayer controller keeps the RBAC it + // needs to watch targets and DELETE the EnvoyExtensionPolicies it generated + // while WAF is disabled (EV-6751). The rule set is identical enabled vs + // disabled, so toggling waf.state causes no ClusterRole churn. rules = append(rules, // Application-layer (gateway-addons) reconcilers reconcile WAF resources // against Gateway API targetRefs and emit events on the policy objects. @@ -742,11 +760,20 @@ func (c *kubeControllersComponent) controllersDeployment() *appsv1.Deployment { env = append(env, corev1.EnvVar{Name: "MULTI_INTERFACE_MODE", Value: c.cfg.Installation.CalicoNetwork.MultiInterfaceMode.Value()}) } - // Application-layer (gateway-addons / WAF v3) env vars, gated by + // The WAF reconcilers are wired whenever Gateway API is present (see the + // applicationlayer entry in enabledControllers), so they can tear down the + // EnvoyExtensionPolicies they generated when WAF is disabled. + // WAF_GATEWAY_EXTENSION_ENABLED tells them whether to program (enabled) or + // de-program (disabled) — EV-6751. Absent ⇒ the reconciler defaults to + // enabled, so an older operator that predates this var is unaffected. + if c.cfg.GatewayAPIPresent { + env = append(env, corev1.EnvVar{Name: "WAF_GATEWAY_EXTENSION_ENABLED", Value: strconv.FormatBool(c.cfg.WAFGatewayExtensionEnabled)}) + } + + // Application-layer (gateway-addons / WAF v3) WASM env vars, gated by // GatewayAPI.spec.extensions.waf.state == Enabled. When the gate is // off (default), none of the WASM_* env vars are rendered and the - // kube-controllers binary skips the WAF reconcilers entirely (see the - // applicationlayer entry in enabledControllers). + // WAF reconcilers de-program rather than attach a filter. if c.cfg.WAFGatewayExtensionEnabled { // Application-layer (gateway-addons) reconcilers consume the Coraza WAF // wasm OCI reference from this env var to program WAF policy attachments. diff --git a/pkg/render/kubecontrollers/kube-controllers_test.go b/pkg/render/kubecontrollers/kube-controllers_test.go index 6d153433d0..7de097620b 100644 --- a/pkg/render/kubecontrollers/kube-controllers_test.go +++ b/pkg/render/kubecontrollers/kube-controllers_test.go @@ -264,6 +264,7 @@ var _ = Describe("kube-controllers rendering tests", func() { cfg.MetricsPort = 9094 // Opt in to the WAF Gateway API add-on so the WAF env vars + RBAC are rendered. cfg.WAFGatewayExtensionEnabled = true + cfg.GatewayAPIPresent = true cfg.WAFWebhookCABundle = []byte("fake-ca-bundle") // core_controller provisions a dedicated WAF wasm pull secret (a renamed // copy of the install pull secret) so the reconciler can replicate it into @@ -460,6 +461,7 @@ var _ = Describe("kube-controllers rendering tests", func() { cfg.MetricsPort = 9094 // Opt in to the WAF Gateway API add-on so the WAF env vars + RBAC are rendered. cfg.WAFGatewayExtensionEnabled = true + cfg.GatewayAPIPresent = true component := kubecontrollers.NewElasticsearchKubeControllers(&cfg) Expect(component.ResolveImages(nil)).To(BeNil()) @@ -531,6 +533,7 @@ var _ = Describe("kube-controllers rendering tests", func() { cfg.MetricsPort = 9094 // Opt in to the WAF Gateway API add-on so the WAF env vars + RBAC are rendered. cfg.WAFGatewayExtensionEnabled = true + cfg.GatewayAPIPresent = true component := kubecontrollers.NewCalicoKubeControllers(&cfg) Expect(component.ResolveImages(nil)).To(BeNil()) @@ -661,6 +664,7 @@ var _ = Describe("kube-controllers rendering tests", func() { instance.Variant = operatorv1.CalicoEnterprise cfg.WAFGatewayExtensionEnabled = true + cfg.GatewayAPIPresent = true cfg.WAFWebhookServerTLS = wafTLS component := kubecontrollers.NewCalicoKubeControllers(&cfg) @@ -694,6 +698,41 @@ var _ = Describe("kube-controllers rendering tests", func() { })) }) + It("should keep the WAF controller wired for teardown but render no active WAF surface when WAF is disabled (EV-6751)", func() { + instance.Variant = operatorv1.CalicoEnterprise + // GatewayAPI present but WAF turned off: the applicationlayer controller + // must stay wired (with its EnvoyExtensionPolicy delete RBAC) so it can + // tear down the EEPs it generated, and be told it is disabled via the env. + cfg.GatewayAPIPresent = true + cfg.WAFGatewayExtensionEnabled = false + + component := kubecontrollers.NewCalicoKubeControllers(&cfg) + Expect(component.ResolveImages(nil)).To(BeNil()) + resources, _ := component.Objects() + + dp := rtest.GetResource(resources, kubecontrollers.KubeController, common.CalicoNamespace, "apps", "v1", "Deployment").(*appsv1.Deployment) + c := dp.Spec.Template.Spec.Containers[0] + + // Controller stays wired. + Expect(c.Env).To(ContainElement(corev1.EnvVar{ + Name: "ENABLED_CONTROLLERS", Value: "node,loadbalancer,service,federatedservices,usage,applicationlayer", + })) + // Told it is disabled → the reconciler de-programs rather than attaches. + Expect(c.Env).To(ContainElement(corev1.EnvVar{Name: "WAF_GATEWAY_EXTENSION_ENABLED", Value: "false"})) + // No active WAF surface: no WASM image env, no webhook cert dir. + for _, e := range c.Env { + Expect(e.Name).NotTo(Equal("WASM_IMAGE")) + Expect(e.Name).NotTo(Equal("WAF_WEBHOOK_CERT_DIR")) + } + // But it keeps the EnvoyExtensionPolicy delete RBAC to run the teardown. + clusterRole := rtest.GetResource(resources, "calico-kube-controllers", "", "rbac.authorization.k8s.io", "v1", "ClusterRole").(*rbacv1.ClusterRole) + Expect(clusterRole.Rules).To(ContainElement(rbacv1.PolicyRule{ + APIGroups: []string{"gateway.envoyproxy.io"}, + Resources: []string{"envoyextensionpolicies"}, + Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}, + })) + }) + It("should render all es-calico-kube-controllers resources for a default configuration using CalicoEnterprise and ClusterType is Management", func() { expectedResources := []struct { name string @@ -720,6 +759,7 @@ var _ = Describe("kube-controllers rendering tests", func() { cfg.MetricsPort = 9094 // Opt in to the WAF Gateway API add-on so the WAF env vars + RBAC are rendered. cfg.WAFGatewayExtensionEnabled = true + cfg.GatewayAPIPresent = true component := kubecontrollers.NewElasticsearchKubeControllers(&cfg) Expect(component.ResolveImages(nil)).To(BeNil())