Skip to content

PMREQ-821: Whisker access via Calico Ingress Gateway - #5146

Open
vara2504 wants to merge 11 commits into
tigera:masterfrom
vara2504:vara-PMREQ-821-cig-whisker
Open

PMREQ-821: Whisker access via Calico Ingress Gateway#5146
vara2504 wants to merge 11 commits into
tigera:masterfrom
vara2504:vara-PMREQ-821-cig-whisker

Conversation

@vara2504

@vara2504 vara2504 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Description

New feature: expose the Whisker UI (Calico OSS) through Calico Ingress Gateway, following the same flow #5032 added for Manager.

  • Adds spec.ingressGateway to the Whisker CR. When set, the Whisker controller renders a Gateway, HTTPRoute, Backend, and ReferenceGrant, mints a gateway TLS secret, and reports Degraded until the Gateway is programmed. The gateway is rendered only on the Calico variant: Whisker itself is deleted on other variants, so a gateway there would point at a Service the same reconcile is removing. On those variants the controller tears the gateway down instead.
  • Extracts the controller-side gateway logic shared with Manager into pkg/uigateway: a Helper built with NewHelper(client, Config) whose single entrypoint, Components, folds in the GatewayAPI fetch, class resolution, namespace creation, the certificateManagement guard, and stale-namespace cleanup — each controller makes one call. Failures come back as plain errors; the controller reports Degraded and returns the error. No Manager behavior change beyond the shared refactor.
  • The common gateway code carries no variant knowledge. Variant-specific objects (the WAF filter's ServiceAccount and RoleBinding) are supplied by the caller through Config.ExtraProxyObjects, built by the new pkg/enterprise/uigateway for Manager and nil for Whisker. This also removes a variant-switch hazard: a Whisker teardown can no longer delete the shared-name WAF ServiceAccount a Manager gateway depends on.
  • Narrows the operator's Gateway API write permissions to the namespaces the user configured. The cluster-scoped ClusterRole keeps only get/list/watch on gateways, httproutes, referencegrants and backends — the controller-runtime cache needs reads in every namespace. For writes, the operator grants itself one namespaced Role and RoleBinding per purpose, bound to its own ServiceAccount: <prefix>-ingressgateway-access (gateways, httproutes) in the gateway namespace and <prefix>-ingressgateway-backend-access (referencegrants, backends) in the backend namespace. This follows the existing waypoint EnvoyFilter Role pattern and relies on the operator's bind/escalate verbs. Manager gets the same treatment, since both components share pkg/render/gateway.
    Requires the companion chart changes that drop the cluster-wide write verbs: PMREQ-821: Grant the operator Gateway API access for Whisker CIG projectcalico/calico#13521 (OSS) and tigera/calico-private#13247 (Enterprise).
  • A namespace the operator creates is owned and cleaned up. gatewayNamespace naming a namespace that does not exist is created through the same builder as the install namespace — so on OpenShift it carries the labels that let the Envoy proxy pod pass admission — and stamped with the gateway label; teardown deletes it. A namespace the user already had is never labeled and never deleted.
  • The ReferenceGrant is rendered at v1beta1. The standard has not promoted it to v1, so a cluster serving pre-installed Gateway API CRDs (OpenShift 4.19+ ships them) has no v1 to match; at v1 the create silently failed there and the teardown erred forever.
  • Ordering is asserted by tests, in both directions: the access grants are created before any resource they permit writing, the Gateway is created before the resources that cleanup later discovers through its label, and on delete the grants are dropped after the resources whose deletion they authorize, with an owned namespace last. A dedicated test asserts every rendered object has a matching delete, so the create and delete paths cannot drift silently.
  • The HTTPRoute sets a 0s request timeout so SSE flow-log streams stay open; the Whisker NetworkPolicy admits only this Gateway's Envoy proxy pods on 8443, selecting their namespace by kubernetes.io/metadata.name; the gateway re-originates TLS to Whisker's HTTPS port, validated against the trusted CA bundle.
  • Renders the gateway proxy NetworkPolicy on both variants (calico-system has an operator-managed default-deny on Calico too) and creates the operator-secrets RoleBinding in gateway namespaces on both variants — the latter in the GatewayAPI controller's common per-namespace path, beside the variant extension's additions.

Testing

  • Operator UTs and the gateway FV suite pass.
  • Whisker, Calico on AWS (EKS) and on OpenShift 4.20. Full lifecycle with the narrowed ClusterRole applied: gateway in calico-system (split grants self-provisioned, no WAF ServiceAccount on OSS, OpenShift DNS egress rules in the proxy NetworkPolicy), UI served through the gateway over TLS with SNI, a move to an operator-created namespace (ownership label present, stale objects cleaned in the same reconcile, Backend retained, cross-namespace ReferenceGrant created at v1beta1 with ResolvedRefs=True), and teardown (all objects removed, the owned namespace deleted). E2E specs (Feature:Ingress-Gateway) pass on OpenShift.
  • Manager, Enterprise on GKE and on OpenShift 4.20. Same narrowed ClusterRole and lifecycle, including the Enterprise-only WAF ServiceAccount/RoleBinding via pkg/enterprise/uigateway. On Enterprise OpenShift, a stock-operator Manager degrade out of the box ("no matches for kind Backend") cleared on this branch — the removed teardown backstop was force-including the install namespace whenever the pre-installed Gateway API CRDs were present.
  • OIDC through the gateway (Enterprise). With Authentication.spec.managerDomain pointed at the gateway hostname and the two callback URLs registered, the whole Dex redirect chain completes headlessly; the id_token is accepted by Manager's API.

Known limitations

  • Cleanup finds this feature's resources by the operator.tigera.io/gateway label on the Gateway, so anything created before the Gateway is invisible to it if the Gateway itself never appears. The access Role and RoleBinding are rendered first, because they carry the permissions everything else needs; if the render fails between them and the Gateway, and spec.ingressGateway is cleared before the next reconcile converges, they are left behind. Not an escalation: the leftovers grant write access on Gateway API kinds in a single namespace to the operator's own ServiceAccount, and re-setting spec.ingressGateway re-adopts them. Accepted to keep cleanup single-sourced on the Gateway.
  • On OpenShift, a user-supplied gatewayNamespace (one that already exists) needs SecurityContextConstraints access for the Envoy proxy pod; only a namespace the operator creates carries the required labels. This is inherited from the GatewayAPI feature — any Gateway in a plain custom namespace on OpenShift is affected — and is tracked separately.

Release Note

The Whisker UI can be exposed through Calico Ingress Gateway by setting spec.ingressGateway on the Whisker resource.

@vara2504
vara2504 requested review from a team and marvin-tigera as code owners August 6, 2026 23:49
@marvin-tigera marvin-tigera added this to the v1.44.0 milestone Aug 6, 2026
@vara2504
vara2504 marked this pull request as draft August 6, 2026 23:52
@vara2504
vara2504 force-pushed the vara-PMREQ-821-cig-whisker branch 2 times, most recently from dbb9669 to 16a3367 Compare August 13, 2026 05:17
@vara2504
vara2504 marked this pull request as ready for review August 13, 2026 05:26
Copilot AI lite review requested due to automatic review settings August 13, 2026 16:19
@vara2504
vara2504 force-pushed the vara-PMREQ-821-cig-whisker branch from 16a3367 to eddfec4 Compare August 13, 2026 16:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Calico Ingress Gateway (CIG) exposure support for the Whisker UI by introducing a spec.ingressGateway configuration on the Whisker CR and reusing/refactoring the existing Manager gateway implementation into shared controller logic (pkg/controller/uigateway) and enhanced gateway rendering (pkg/render/gateway).

Changes:

  • Add spec.ingressGateway to the Whisker API/CRD and reconcile/render Gateway API resources (Gateway/HTTPRoute/Backend/ReferenceGrant/TLS Secret) when configured.
  • Extract shared UI-gateway controller behaviors (watches, cleanup, namespace provisioning, class resolution, health read-back) into pkg/controller/uigateway and wire Manager/Whisker controllers to use it.
  • Extend gateway rendering to support configurable HTTPRoute request timeouts and introduce namespace-scoped “writer” RBAC to confine Gateway API write verbs to configured namespaces.

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pkg/render/whisker/component.go Adds gateway-aware ingress rule to Whisker NetworkPolicy when CIG is configured.
pkg/render/whisker/component_test.go Tests NetworkPolicy behavior with/without ingress gateway namespace.
pkg/render/gateway/component.go Adds route request timeout support, writer RBAC objects, and adjusts render/delete ordering and variant behavior.
pkg/render/gateway/component_test.go Updates/extends gateway render & deletion tests for writer RBAC, NP behavior, and timeouts.
pkg/imports/crds/operator/operator.tigera.io_whiskers.yaml Adds spec.ingressGateway schema to Whisker CRD.
pkg/imports/crds/operator/operator.tigera.io_managers.yaml Aligns IngressGatewaySpec docs to “degrades until specified” behavior.
pkg/controller/whisker/controller.go Implements Whisker ingress gateway reconciliation, cleanup, TLS minting, and health gating.
pkg/controller/whisker/controller_test.go Adds reconciliation tests for gateway resources, TLS persistence, unhealthy requeue, and variant gating.
pkg/controller/uigateway/uigateway.go New shared helper for gateway cleanup discovery, class resolution, namespace creation, watch setup, and health read-back.
pkg/controller/uigateway/uigateway_test.go Unit tests for shared gateway health and cleanup helper behaviors.
pkg/controller/uigateway/uigateway_suite_test.go New Ginkgo suite wiring for uigateway tests.
pkg/controller/manager/manager_controller.go Refactors Manager gateway watch/cleanup/health logic to use uigateway.
pkg/controller/manager/manager_controller_test.go Adds/updates test coverage for gateway missing-GatewayAPI degrade behavior.
pkg/controller/manager/gateway_status_test.go Removes Manager-specific gateway status tests now covered by shared uigateway tests.
pkg/controller/gatewayapi/gatewayapi_controller.go Ensures operator-secrets RoleBinding is written in gateway namespaces on both variants.
pkg/controller/gatewayapi/gatewayapi_controller_test.go Adds Calico-variant test for per-namespace bundle + operator-secrets RoleBinding (no WAF resources).
api/v1/whisker_types.go Adds IngressGateway *IngressGatewaySpec to WhiskerSpec with kubebuilder optional semantics.
api/v1/ingress_gateway_types.go Updates IngressGatewaySpec docs to match “component degrades” behavior.
api/v1/zz_generated.deepcopy.go Regenerates deepcopy for new WhiskerSpec field.
Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/render/gateway/component.go Outdated
Comment on lines +108 to +116
// The writer Role comes first: it is what carries the write verbs for the
// kinds below, so it has to exist before them. The first reconcile's writes
// may still be denied while the authorizer catches up; the requeue succeeds,
// the same way the TLS secret does below.
objs = append(objs, writerObjects(c.cfg.ResourcePrefix, c.cfg.GatewayNamespace)...)
if c.cfg.GatewayNamespace != c.cfg.BackendNamespace {
// The Backend and ReferenceGrant are written in the backend namespace.
objs = append(objs, writerObjects(c.cfg.ResourcePrefix, c.cfg.BackendNamespace)...)
}
@vara2504
vara2504 force-pushed the vara-PMREQ-821-cig-whisker branch from eddfec4 to c6e0456 Compare August 13, 2026 17:48
Copilot AI review requested due to automatic review settings August 13, 2026 17:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file
Suppressed comments (1)

pkg/imports/crds/operator/operator.tigera.io_whiskers.yaml:62

  • The Whisker CRD's spec.ingressGateway.hostname description references Authentication managerDomain (and notes “Manager only”), which is confusing/unrelated for Whisker users. Since this YAML is generated, the underlying Go type comment should be updated so the generated Whisker CRD docs are component-appropriate (e.g., describe Whisker behavior only, or clearly separate Manager vs Whisker semantics), then re-run code generation to refresh this file.
                    hostname:
                      description: |-
                        Hostname for the Gateway listener. Must match the Authentication CR's
                        managerDomain when OIDC is configured (Manager only).
                      minLength: 1

Copilot AI review requested due to automatic review settings August 13, 2026 18:01
@vara2504
vara2504 force-pushed the vara-PMREQ-821-cig-whisker branch from c6e0456 to 55fdf02 Compare August 13, 2026 18:01
@vara2504
vara2504 force-pushed the vara-PMREQ-821-cig-whisker branch from 55fdf02 to 9627a4a Compare August 13, 2026 18:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file

Comment on lines +116 to +132
// Teardown returns deletion components for every labeled Gateway namespace,
// plus the backend namespace, which contains the Backend and ReferenceGrant.
//
// If no labeled Gateway exists, nothing is returned. The Gateway is rendered
// before any other gateway resources, so those resources cannot exist without
// a corresponding Gateway. This also avoids touching kinds the cluster may not
// serve: Backend is an Envoy Gateway resource, which may be unavailable when
// the Gateway API CRDs were installed independently. Attempting to delete an
// unserved kind would fail the reconcile.
func (c *Config) Teardown(ctx context.Context) ([]render.Component, error) {
namespaces, gatewayCRDsPresent, err := c.Namespaces(ctx)
if err != nil {
return nil, err
}
if !gatewayCRDsPresent || len(namespaces) == 0 {
return nil, nil
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This version creates the access Role and RoleBinding first. The behavior is essentially the same: RBAC needs to exist before the Gateway can be created.

I'm keeping the current behavior for this PR. Hitting this requires a failed reconcile and the user clearing spec.ingressGateway before the retry. Otherwise, the Gateway is created, cleanup finds everything by label, and re-setting the field re-adopts the resources.

Bringing back the backstop would require another cluster-wide Role/RoleBinding discovery path and additional permissions, which doesn't seem worth the complexity right now.

Copilot AI review requested due to automatic review settings August 13, 2026 18:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file
Suppressed comments (2)

pkg/render/gateway/component.go:532

  • In the move-cleanup path, this deletion component will also drop the writer Role/RoleBinding when the old gateway namespace equals the backend namespace (gwNS == bkNS). That contradicts the comment (“backend namespace keeps its grant”) and can leave a window where subsequent teardown (spec removal) loses the namespace-scoped write RBAC needed to delete backend/route resources.
	drop := writerObjects(prefix, gwNS)
	if gwNS != bkNS && !move {
		drop = append(drop, writerObjects(prefix, bkNS)...)
	}

pkg/render/gateway/component.go:187

  • The gateway writer Role currently grants update/delete on all Gateways/HTTPRoutes/ReferenceGrants/Backends in the namespace. This is broader than necessary (it could affect user-managed Gateway API resources in that namespace) even though the operator only intends to manage its own named resources.
				{
					APIGroups: []string{gapi.GroupName},
					Resources: []string{"gateways", "httproutes", "referencegrants"},
					Verbs:     []string{"create", "update", "delete"},
				},

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file
Suppressed comments (3)

pkg/controller/uigateway/uigateway.go:130

  • Teardown() assumes no component-owned resources can exist unless a labeled Gateway exists, but pkg/render/gateway.Component.Objects() now renders the writer Role/RoleBinding before the Gateway. If Gateway creation fails (e.g., Gateway CRDs missing, webhook rejection), those RBAC objects can be left behind and Teardown() will return early (len(namespaces)==0) and never clean them up.
	if !gatewayCRDsPresent || len(namespaces) == 0 {

pkg/render/gateway/component.go:195

  • The writer RoleBinding is rendered without the component's gateway cleanup label. Adding the same label used on the Gateway makes it possible to discover/clean up these grants even if the Gateway never gets created.
		ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},

pkg/render/gateway/component.go:173

  • The writer Role is rendered without the component's gateway cleanup label. If a reconcile creates this Role but fails before creating the labeled Gateway, label-driven cleanup (via listing labeled Gateways) cannot discover the namespace later, leaving the RBAC grant behind.

This issue also appears on line 195 of the same file.

		ObjectMeta: metav1.ObjectMeta{Name: WriterRoleName(resourcePrefix), Namespace: namespace},

TLSSecretName: ManagerGatewayTLSSecretName,
BackendNamespace: helper.InstallNamespace(),
Enterprise: installationSpec.Variant.IsEnterprise(),
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is called a "helper" but the struct is just config, which is a bit unusual.

If we want this to fit the normal pattern, we should do something like this:

gwHelper := uigateway.NewHelper(...)
func NewHelper(...) Helper { }
type Helper struct // Or interface{}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — Config is now data only and Helper owns the client via NewHelper(client, Config), the same split the Reconciler structs use.

TLSSecretName: ManagerGatewayTLSSecretName,
BackendNamespace: helper.InstallNamespace(),
Enterprise: installationSpec.Variant.IsEnterprise(),
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is called a "helper" but the struct is just config, which is a bit unusual.

If we want this to fit the normal pattern, we should do something like this:

gwHelper := uigateway.NewHelper(...)
func NewHelper(...) Helper { }
type Helper struct // Or interface{}, for testing purposes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}
} else if instance.Spec.IngressGateway != nil {
gwComp, gwKeyPair, result, err := r.resolveGateway(ctx, instance, authenticationCR, certificateManager, helper, logc)
gwComp, gwKeyPair, result, err := r.resolveGateway(ctx, instance, installationSpec, authenticationCR, certificateManager, helper, logc)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many of the other funcs moved to the helper - should this one as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveGateway is Manager-specific: the OIDC managerDomain check needs the Authentication CR, which Whisker doesn't have, and the listener keypair has to flow back to the controller for the certificate component.

Comment thread pkg/controller/uigateway/uigateway.go Outdated

// Config identifies one UI component's gateway resources.
type Config struct {
Client client.Client

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit of a smell - a Config struct shouldn't contain a client, at least how we tend to structure the rest of the operator code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the client moved to NewHelper; Config holds only data, matching how the reconcilers hold cli themselves.

Comment thread pkg/controller/uigateway/uigateway.go Outdated
Comment on lines +61 to +64
// Enterprise controls whether the proxy SA and RoleBinding are part of
// the component's rendered set; the proxy NetworkPolicy is rendered on
// both variants.
Enterprise bool

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably better to pass the Variant here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went one further: neither is passed now. The common code carries no variant knowledge — callers supply Config.ExtraProxyObjects (Manager passes what pkg/enterprise/uigateway builds, Whisker passes nil), so there's no flag left to derive or default.

Comment thread pkg/render/gateway/component.go Outdated
ResourcePrefix string
GatewayNamespace string
ResourcePrefix string
// StaleNamespace is the namespace being cleaned up.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// StaleNamespace is the namespace being cleaned up.

Claude loves to throw obvious comments in above new struct fields even when no other field on the struct has them...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted.

@@ -383,19 +495,20 @@ func (c *gatewayDeletionComponent) Objects() (objsToCreate, objsToDelete []clien
objs := []client.Object{
&corev1.Secret{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly just curious... do you know why we have a gateway deletion component instead of just using the gateway component and inverting create / delete?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inverting would work only by faking the config: the create path needs a hostname, class, and TLS keypair to build its objects, and all of those come from spec.ingressGateway, which is nil during teardown. The delete set is also not a mirror of the create set — teardown covers every labelled namespace, keeps the Backend on a move, and must not touch a custom namespace's SA/RoleBinding, so those rules would become delete-only branches inside a shared component anyway. To keep the two paths honest, a test now asserts every rendered object has a matching delete, so a missed mirror fails CI rather than leaking.

@@ -383,19 +495,20 @@ func (c *gatewayDeletionComponent) Objects() (objsToCreate, objsToDelete []clien
objs := []client.Object{
&corev1.Secret{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly just curious... do you know why we have a gateway deletion component instead of just using the gateway component and inverting create / delete? Seems like we need to be careful to keep these in sync?

Comment on lines +554 to +556
// The Gateway goes after the resources found through it, mirroring the render.
// If an earlier delete fails, it stays and the next reconcile still finds the
// leftovers by its label.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a finalizer is the correct way to do this? Relying on ordering can be finicky / error-prone, and finalizers are the k8s native way to say "keep this around until I am done with it".

Let's not do it in this PR, but perhaps as a follow-on to keep things tidy?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed as a follow-up.

Action: v3.Allow,
Protocol: &networkpolicy.TCPProtocol,
Source: v3.EntityRule{
NamespaceSelector: fmt.Sprintf("%s == '%s'", selector.CalicoNameLabel, c.cfg.IngressGatewayNamespace),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to self: we should rename the CalicoNameLabel

Copilot AI review requested due to automatic review settings August 25, 2026 19:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file
Suppressed comments (1)

pkg/uigateway/uigateway.go:201

  • Teardown’s doc comment says “the Gateway is rendered first, so nothing else can exist without one”, but pkg/render/gateway now renders access Role/RoleBinding before the Gateway. Even if the current cleanup behavior is intentional, the comment should reflect the actual ordering/limitation so future readers don’t assume RBAC can’t exist without a labeled Gateway.
// Teardown returns deletion components for every labeled Gateway namespace,
// plus the backend namespace, which holds the Backend and ReferenceGrant.
// No labeled Gateway means nothing to do: the Gateway is rendered first, so
// nothing else can exist without one.

Comment thread pkg/uigateway/uigateway.go Outdated
Comment on lines +301 to +303
// EnsureNamespace creates the gateway namespace if it does not exist.
// The namespace is created without an owner reference and is never deleted by
// the operator: a user-provided namespace may hold other workloads.
Comment thread pkg/uigateway/uigateway_suite_test.go Outdated
gomega.RegisterFailHandler(ginkgo.Fail)
suiteConfig, reporterConfig := ginkgo.GinkgoConfiguration()
reporterConfig.JUnitReport = "../../../report/ut/uigateway_suite.xml"
ginkgo.RunSpecs(t, "pkg/controller/uigateway Suite", suiteConfig, reporterConfig)
Comment on lines +15 to +20
// Package uigateway carries the controller-side logic shared by the UI
// components (Manager, Whisker) that expose themselves through Calico
// Ingress Gateway: label-driven cleanup, gateway health read-back, namespace
// provisioning, class resolution, and watch setup. The rendering lives in
// pkg/render/gateway; this package holds what a reconciler needs around it.
package uigateway

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aligned — the description now says pkg/uigateway (with pkg/enterprise/uigateway layering the Enterprise objects on top).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

vara2504 and others added 8 commits August 25, 2026 13:13
Move the gateway helper logic out of the manager controller into a
shared package so the Whisker controller can reuse it: label-driven
namespace listing and cleanup, gateway/route health read-back,
namespace provisioning, class resolution, and watch setup.

The manager controller now delegates to uigateway.Config; manager-only
logic (multi-tenant guard, managerDomain host check) stays in place.
No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds spec.ingressGateway to the Whisker CR. When set, the controller renders a
Gateway, HTTPRoute, Envoy Gateway Backend and ReferenceGrant, mints the listener
certificate, and reports Degraded until the Gateway is programmed.

The HTTPRoute disables the request timeout so SSE flow-log streams stay open,
the whisker NetworkPolicy admits only this gateway's proxy pods on 8443, and the
gateway re-originates TLS to Whisker's HTTPS port against the trusted bundle.
The proxy NetworkPolicy and the operator-secrets RoleBinding render on both
variants: calico-system carries a default-deny on Calico too, and the operator
needs secret access in a custom gateway namespace either way.

Nothing is rendered on a non-Calico variant, where Whisker itself is deleted.
Cleanup keys off the labelled Gateway alone, which is rendered before every
other resource and deleted after them. Write access comes from a Role the
operator self-grants per namespace rather than from the cluster-wide ClusterRole.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename the per-namespace Role and RoleBinding from <prefix>-gateway-writer
to <prefix>-ingressgateway-access, matching how the operator names the
grants it gives its own ServiceAccount (tigera-operator-secrets,
tigera-waypoint-l7-envoyfilters) rather than naming them after verbs.

Label both with operator.tigera.io/gateway so they are discoverable the
same way as the Gateway they exist for.

Drop the unused bool from Namespaces(): both callers discarded it, since
an unserved Gateway kind already yields no namespaces.
Split the namespaced grant per purpose: the gateway namespace gets gateways
and httproutes, the backend namespace referencegrants and backends. Neither
namespace holds verbs it never uses, and no single object serves two purposes
when the namespaces coincide — which is what made cleanup drop a grant the
Backend still depended on. Upgrades remove the combined Role left behind.

Delete only what a cleanup run's own namespace holds, so a run cannot revoke
a grant a later one still needs and then leave it unable to finish.

Degrade instead of writing a TLS secret with no private key when
certificateManagement is enabled, and say what a missing GatewayAPI CR costs
rather than implying only gateway resources are skipped.

Take both Enterprise flags from the installation variant, rename the deletion
component's namespace field to StaleNamespace, and trim comments to what the
code does not already say.
Two of the four were hardcoded, so the same question was answered two ways in
one file. resolveGateway takes the installation spec to do it.
Config is now data only; Helper owns the client via NewHelper, and one
entrypoint, Components, folds in the GatewayAPI fetch, class resolution,
namespace creation, and the certificateManagement guard, so both controllers
make a single call. Conditions come back as a typed Error; controllers set
Degraded themselves and no longer requeue on configuration problems, since
the CRs that fix them are watched.

The package moves to pkg/uigateway with no variant knowledge: callers supply
ExtraProxyObjects, built by the new pkg/enterprise/uigateway for Manager and
nil for Whisker. That also stops a Whisker teardown on a variant switch from
deleting the shared WAF ServiceAccount Manager's gateway depends on.

A namespace the operator creates is labelled and deleted on teardown; one the
user already had is never touched. MoveCleanup is renamed StaleComponents,
the never-shipped pre-split Role cleanup is deleted, and a new test asserts
every rendered object has a matching delete.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The typed Error carried a reason and message so callers could degrade without
requeueing, but the branch it required outweighed what it bought: the specific
cause still reaches TigeraStatus through the error chain. Components now
returns plain errors, both controllers degrade with one generic reason and
return the error, and controller-runtime's backoff replaces the watch-only
wait on configuration problems.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Master moved the computed installation into status and the variant objects
behind the extensions boundary, so the tests mutate Status.Computed and the
gateway suites use the variant's extension; the operator-secrets RoleBinding
moves to the common per-namespace path, since both variants need it for the
UI gateway TLS secret.

Also addresses review: the suite keeps its old package name, a doc comment
kept an exported spelling, and the namespace comment predated ownership.
Comment duplicates now state each rule once, at the site that enforces it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 25, 2026 20:51
@vara2504
vara2504 force-pushed the vara-PMREQ-821-cig-whisker branch from c05073a to 268d6a6 Compare August 25, 2026 20:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file
Suppressed comments (1)

pkg/uigateway/uigateway.go:19

  • PR description says the shared controller-side gateway logic was extracted into pkg/controller/uigateway, but the new shared package is pkg/uigateway. Please either update the PR description to match the actual package path, or move the package to the documented location so future readers can find it.
// Package uigateway carries the controller-side logic shared by the UI
// components (Manager, Whisker) that expose themselves through Calico
// Ingress Gateway: label-driven cleanup, gateway health read-back, namespace
// provisioning, class resolution, and watch setup. The rendering lives in
// pkg/render/gateway; this package holds what a reconciler needs around it.

Comment on lines +986 to +989
comps, err := gwHelper.Components(ctx, gw, gwTLSKeyPair)
if err != nil {
r.status.SetDegraded(operatorv1.ResourceCreateError, "Failed to render gateway resources", err, logc)
return nil, nil, err

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResourceCreateError is accurate: whatever the cause, the failure blocks creating the gateway resources, and the error chain carries the specific cause into the status message ("Failed to
render gateway resources: GatewayAPI CR not found; …")

Comment on lines +310 to +314
gatewayComponents, err = gwHelper.Components(ctx, gw, gatewayTLSKeyPair)
if err != nil {
r.status.SetDegraded(operatorv1.ResourceCreateError, "Failed to render gateway resources", err, reqLogger)
return reconcile.Result{}, err
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResourceCreateError is valid: whatever the cause, the failure blocks creating the gateway resources, and the error chain carries the specific cause into the status message ("Failed to
render gateway resources: GatewayAPI CR not found; …")

State why the Whisker render needs the gateway namespace and where the
Enterprise proxy objects apply, and trim the grants-ordering comment to the
ordering rationale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 25, 2026 22:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file

Comment on lines +517 to 530
// The grants go after the resources they permit deleting. The backend grant
// is dropped only by the backend namespace's own component.
objs = append(objs, c.roleBinding(staleNS, gatewayAccessSuffix), c.role(staleNS, gatewayAccessSuffix))
if staleNS == bkNS && c.cfg.TargetNamespace == "" {
objs = append(objs, c.roleBinding(bkNS, backendAccessSuffix), c.role(bkNS, backendAccessSuffix))
}

// The namespace goes last. Deleting it removes everything inside.
if c.cfg.DeleteNamespace {
objs = append(objs, &corev1.Namespace{
TypeMeta: metav1.TypeMeta{Kind: "Namespace", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{Name: staleNS},
})
}
The standard has not promoted ReferenceGrant to v1, so a cluster serving
pre-installed Gateway API CRDs — OpenShift 4.19+ ships them — has no v1 to
match. The create silently failed there, leaving cross-namespace references
unresolved, and the teardown erred forever, orphaning the backend grant.
v1beta1 is served everywhere; the scheme now registers it.

Also point the moved suite's JUnit report back inside the repository; the
old depth wrote above it and failed CI on permission.

Verified on OpenShift 4.20: cross-namespace ReferenceGrant created and
resolved, teardown clean, and a stock-operator Manager degrade on Enterprise
OpenShift cleared by the earlier backstop removal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 25, 2026 23:36
@vara2504

Copy link
Copy Markdown
Contributor Author

@caseydavenport Addressed the review comments. Should be good for another look when you have a chance.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file
Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

api/v1/whisker_types.go:57

  • The Whisker API doc comment for spec.ingressGateway doesn’t mention that the controller only renders the gateway on the Calico variant (and will tear it down on other variants). This can mislead users who set the field on Enterprise/non-Calico installations and see it ignored.
	// IngressGateway configures Calico Ingress Gateway access to the Whisker UI.
	// When set, the operator renders Gateway API resources (Gateway, HTTPRoute,
	// Backend, ReferenceGrant, TLS Secret) to expose Whisker via CIG.
	// Requires a GatewayAPI CR to be present.
	// +optional
	IngressGateway *IngressGatewaySpec `json:"ingressGateway,omitempty"`

pkg/uigateway/uigateway.go:200

  • The Teardown doc comment says “the Gateway is rendered first, so nothing else can exist without one”, but the gateway renderer now creates access Role/RoleBinding before the Gateway. Even if the known limitation is accepted, this comment is now inaccurate and should reflect the current behavior/limitation.
// Teardown returns deletion components for every labeled Gateway namespace,
// plus the backend namespace, which holds the Backend and ReferenceGrant.
// No labeled Gateway means nothing to do: the Gateway is rendered first, so
// nothing else can exist without one.

… namespace

On OpenShift a plain namespace rejects the Envoy proxy pod: SCC
admission forbids its privileged init container and fixed UIDs.
Create the gateway namespace through render.CreateNamespace so it
carries the same run-level and pod-security labels as the install
namespace, then stamp the ownership label on top.

Config now carries the Provider instead of an OpenShift bool, so
the helper derives platform behavior itself.

A user-supplied existing namespace is still the user's to set up;
that gap applies to the whole GatewayAPI feature and is tracked
separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 25, 2026 23:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • api/v1/zz_generated.deepcopy.go: Generated file

Comment on lines +272 to +276
// unhealthyCondition returns a message when the named condition exists and is
// not True. A missing condition is healthy: the controller has not written
// its verdict yet, and Accepted/Programmed gate readiness once it does.
func unhealthyCondition(conditions []metav1.Condition, condType, msgPrefix string) string {
for _, cond := range conditions {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants