Skip to content

Support enterprise whisker-backend with Linseed upstream - #4861

Closed
hjiawei wants to merge 1 commit into
tigera:masterfrom
hjiawei:hjiawei/whisker-commercial-flow-logs
Closed

Support enterprise whisker-backend with Linseed upstream#4861
hjiawei wants to merge 1 commit into
tigera:masterfrom
hjiawei:hjiawei/whisker-commercial-flow-logs

Conversation

@hjiawei

@hjiawei hjiawei commented May 26, 2026

Copy link
Copy Markdown
Contributor

Description

New feature: deploy whisker-backend for Calico Enterprise, backed by Linseed.

For enterprise the Whisker UI is a manager UI module, so only the whisker-backend container is rendered — no SPA container, nginx config, UI Service or nginx TLS key pair. The backend gets the Linseed upstream env, mTLS client certs, a ClusterRole for Linseed flows plus the RBAC reads it needs (roles/bindings, namespaces, tiers), and Linseed's network policy admits it.

Prerequisites and variants

  • The controller waits for Linseed's certificate (tigera-secure-linseed-cert) before rendering and reports ResourceNotReady until then, so a cluster without log storage degrades instead of running a backend that fails every request.
  • On a managed cluster nothing is deployed and the status stays Available: the manager UI runs on the management cluster, whose whisker-backend serves those flows through the multi-cluster forwarding proxy. The backend is wired for that (MULTI_CLUSTER_FORWARDING_CA/ENDPOINT, egress to Voltron).
  • When an Authentication CR is ready, the backend gets the same Dex settings as the other Dex-authenticating components (OIDC_AUTH_*, Dex CA in the trusted bundle, egress to Dex), and Dex's network policy admits whisker-backend for key fetches. The controller degrades while the Authentication CR is not ready.
  • Watches added for Authentication, ManagementClusterConnection and the Dex TLS secret.

Manager wires the module up when a Whisker CR exists: SUPPORTS_FLOW_LOGS for the UI feature flag, a Voltron /whisker-backend proxy target pointing at the new Service, and the manager/whisker network policies allow that path.

Testing. Render and controller unit tests (Dex env/volumes/egress, MCM env, manager egress, Linseed gate degrade/proceed, managed-cluster no-op; Dex policy goldens). Validated live on a standalone enterprise rig: the operator-rendered Deployment carried the MCM and OIDC env, the Dex policy admitted whisker-backend, and a Dex-issued LDAP user token was accepted end to end through Voltron.

Companion: tigera/calico-private#12086 (whisker-backend), tigera/calico-private#12089 (Whisker CR manifests).

Release Note

Calico Enterprise deploys whisker-backend, backed by Linseed, when a Whisker resource exists on a standalone or management cluster. It waits for Linseed's certificate, accepts Dex-issued user tokens when an Authentication resource is configured, and is not deployed on managed clusters.

For PR author

  • Tests for change.
  • If changing pkg/apis/, run make gen-files (no API changes)
  • If changing versions, run make gen-versions (no version changes)

AI assistance: Claude Code helped write the rendering, controller gating, tests and this description; every change was reviewed and validated live by the author.

@marvin-tigera marvin-tigera added this to the v1.43.0 milestone May 26, 2026
@hjiawei
hjiawei force-pushed the hjiawei/whisker-commercial-flow-logs branch 2 times, most recently from e7aac2c to ed02bd2 Compare May 26, 2026 02:39
@hjiawei
hjiawei marked this pull request as ready for review May 26, 2026 21:22
@hjiawei
hjiawei requested a review from a team as a code owner May 26, 2026 21:22
@hjiawei
hjiawei requested a lite review from Copilot May 26, 2026 21:23

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 enterprise support to the whisker-backend by wiring it to Linseed (instead of Goldmane) for flow data, including RBAC, mTLS, and network policy plumbing for enterprise variants. Non-enterprise (Calico) paths still render Goldmane env/policy as before.

Changes:

  • Render enterprise Whisker ClusterRole/ClusterRoleBinding and Linseed-specific env vars (LINSEED_URL, LINSEED_CA_PATH, LINSEED_TOKEN_PATH, LINSEED_CLUSTER_ID, LINSEED_CLIENT_CERT/KEY_PATH); split network policy into enterprise (Manager ingress + Linseed/kube-apiserver egress) vs Calico (Goldmane egress).
  • Controller: skip Goldmane CR requirement for enterprise, watch TigeraLinseedSecret, switch trusted bundle source to Linseed for enterprise, and add a missing return after SetDegraded in trusted bundle creation.
  • Wire Manager → Whisker egress and Linseed ← Whisker ingress via new WhiskerEntityRule/WhiskerSourceEntityRule helpers; update expected policy JSON fixtures and Manager test egress count.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated no comments.

Show a summary per file
File Description
pkg/render/whisker/component.go Adds enterprise constants, RBAC objects, Linseed env vars, ingress/egress rules in network policy
pkg/render/whisker/component_test.go Adds enterprise object count, deployment, RBAC, and network policy test cases
pkg/render/manager.go Adds Manager egress rule to Whisker
pkg/render/manager_test.go Updates expected egress rule count from 11 to 12
pkg/render/logstorage/linseed/linseed.go Adds Whisker as ingress source to Linseed
pkg/render/common/networkpolicy/networkpolicy.go Adds WhiskerEntityRule and WhiskerSourceEntityRule helpers
pkg/controller/whisker/controller.go Skips Goldmane CR requirement for enterprise, watches Linseed secret, switches trusted bundle source, adds missing return on error
pkg/render/testutils/expected_policies/manager.json Adds Manager→Whisker egress entry
pkg/render/testutils/expected_policies/manager_ocp.json Adds Manager→Whisker egress entry
pkg/render/testutils/expected_policies/linseed.json Adds Whisker ingress entry
pkg/render/testutils/expected_policies/linseed_ocp.json Adds Whisker ingress entry
pkg/render/testutils/expected_policies/linseed_dpi_enabled.json Adds Whisker ingress entry
pkg/render/testutils/expected_policies/linseed_ocp_dpi_enabled.json Adds Whisker ingress entry

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@hjiawei
hjiawei force-pushed the hjiawei/whisker-commercial-flow-logs branch 4 times, most recently from cfbff08 to 8bc0e61 Compare June 3, 2026 05:56
@radTuti radTuti modified the milestones: v1.43.0, v1.44.0 Jun 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for 60 days with no activity.

@github-actions github-actions Bot added the stale label Aug 12, 2026
@danudey danudey modified the milestones: v1.44.0, v1.45.0 Aug 17, 2026
@hjiawei
hjiawei force-pushed the hjiawei/whisker-commercial-flow-logs branch from 8bc0e61 to 5172c8a Compare August 17, 2026 20:57
Copilot AI review requested due to automatic review settings August 17, 2026 20:57

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 12 out of 12 changed files in this pull request and generated no new comments.

Suppressed comments (2)

pkg/controller/whisker/controller.go:222

  • Avoid hard-coding the backend Service name here. Using the shared constant keeps certificate SAN generation aligned with the rendered Service name and prevents drift if the Service name changes in the renderer.
	whiskerBackendCertificateNames := dns.GetServiceDNSNames("whisker-backend", whisker.WhiskerNamespace, r.clusterDomain)

pkg/render/manager.go:114

  • These Whisker-related constants are duplicated from pkg/render/whisker (notably namespace/service name/ports). This creates a drift risk over time; consider moving the shared Service coordinates into the pkg/render package (or another shared package) so both manager rendering and whisker rendering reference the same single source of truth without an import cycle.
	// Whisker-backend Service coordinates, mirroring pkg/render/whisker —
	// importing that package here would create an import cycle (it imports
	// render).
	WhiskerNamespace          = "calico-system"
	WhiskerDeploymentName     = "whisker"
	WhiskerBackendServiceName = "whisker-backend"
	WhiskerBackendServicePort = 8443
	WhiskerBackendTargetPort  = 3002

@github-actions github-actions Bot removed the stale label Aug 18, 2026
@hjiawei
hjiawei force-pushed the hjiawei/whisker-commercial-flow-logs branch from 5172c8a to c31d14c Compare August 18, 2026 00:44
@hjiawei
hjiawei requested review from caseydavenport and pasanw and a balanced review from Copilot August 18, 2026 01:01

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@hjiawei
hjiawei force-pushed the hjiawei/whisker-commercial-flow-logs branch from c31d14c to 4da86bd Compare August 28, 2026 22:01
Copilot AI review requested due to automatic review settings August 28, 2026 22:01

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 29, 2026 18:48
@hjiawei
hjiawei force-pushed the hjiawei/whisker-commercial-flow-logs branch from 4da86bd to 9aa8865 Compare August 29, 2026 18: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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 31, 2026 23:53
@hjiawei
hjiawei force-pushed the hjiawei/whisker-commercial-flow-logs branch from 9aa8865 to b48e64a Compare August 31, 2026 23: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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings September 2, 2026 04:20
@hjiawei
hjiawei force-pushed the hjiawei/whisker-commercial-flow-logs branch from b48e64a to e917d77 Compare September 2, 2026 04:20
For Calico Enterprise the Whisker UI is a manager UI module, so only
the whisker-backend container is deployed — no SPA container, nginx
config, UI Service or nginx TLS key pair. The backend gets the Linseed
upstream env, a ClusterRole granting linseed.tigera.io flows access,
and Linseed's network policy admits it.

The manager wires the module up when a Whisker CR exists: the manager
container gets SUPPORTS_FLOW_LOGS for the UI feature flag, Voltron
gets a /whisker-backend proxy target pointing at the new
whisker-backend Service, and the manager/whisker network policies
allow that path.

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.

🟡 Changes recommended

Managed-cluster cleanup and feature gating are incomplete, and direct OIDC authentication lacks required egress.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 18/18 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment on lines +250 to +254
if mcc != nil {
reqLogger.V(1).Info("Managed cluster: whisker-backend is not deployed; flow logs are served by the management cluster")
r.status.ReadyToMonitor()
r.status.ClearDegraded()
return reconcile.Result{}, nil
Manager: instance,
Authentication: authenticationCR,
KibanaEnabled: kibanaEnabled,
WhiskerEnabled: whiskerCR != nil,
Comment on lines +486 to +492
if c.cfg.KeyValidatorConfig != nil {
// Dex-issued tokens are verified against Dex's signing keys.
egressRules = append(egressRules, v3.Rule{
Action: v3.Allow,
Protocol: &networkpolicy.TCPProtocol,
Destination: render.DexEntityRule,
})
@hjiawei
hjiawei force-pushed the hjiawei/whisker-commercial-flow-logs branch from e917d77 to 7415b51 Compare September 2, 2026 04:26
Copilot AI review requested due to automatic review settings September 2, 2026 04:26

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@pasanw pasanw 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.

The general shape and implementation looks good to me. I audited corner cases with Claude and found some things worth a look


// Clean up the other variant's objects on a variant switch; GC will not,
// since the owning Whisker CR still exists.
if c.isEnterprise() {

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.

Claude: These lists cover the objects Objects() renders, but the whisker-key-pair secret is missed on a Calico -> Calico Enterprise upgrade.

The controller now creates that key pair for OSS only, and skips appending its KeyPairOption when nil. Nothing deletes it: certificatemanagement.go:87 just continues on a nil key pair. So the secret is orphaned in both tigera-operator and calico-system.

Smaller and in the other direction: the OSS branch can't delete the KeyValidatorConfig secret/ConfigMap copies enterprise rendered, because cfg.KeyValidatorConfig is nil there.

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 in the monorepo PRs. BackendOnly now puts the UI key pair on the delete list: the copy in calico-system always, and the truth-namespace secret when the operator issued it (BYO() false), so a user-provided pair stays. The KeyValidatorConfig secret and ConfigMap copies are no longer rendered at all (see the thread on line 172), so the OSS branch has nothing to clean up there. Core side in projectcalico/calico#12879 (staleKeyPair in pkg/controller/whisker/controller.go), carried identically in tigera/calico-private#12086.

Manager: instance,
Authentication: authenticationCR,
KibanaEnabled: kibanaEnabled,
WhiskerEnabled: whiskerCR != nil,

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.

Claude: This turns the flow logs module on for every tenant, but there is only one whisker-backend.

In multi-tenant mode this controller reconciles once per tenant namespace. So each tenant's manager gets SUPPORTS_FLOW_LOGS=true and a Voltron route to whisker-backend.calico-system.svc. That backend is shared, and it is not tenant-scoped. It queries the fixed render.ElasticsearchNamespace Linseed, sends no LINSEED_TENANT_ID, and starts its RBAC calculator with an empty tenant namespace.

At best the tab appears and every request fails, because a tenant's flows live in that tenant's own Linseed. At worst a tenant's user is served another tenant's flows.

The line just above already does the right thing for Kibana: kibanaEnabled := !r.opts.MultiTenant. Suggest the same gate here, for example WhiskerEnabled: whiskerCR != nil && !r.opts.MultiTenant. Making whisker genuinely tenant-aware can be separate work.

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.

Gated as you suggested, plus the managed-cluster case: whiskerEnabled := whiskerCR != nil && managementClusterConnection == nil && !r.opts.MultiTenant. A test in the multi-tenant Describe creates a Whisker CR, reconciles a tenant, and asserts its Voltron gets no whisker route. tigera/calico-private#12086, manager controller commit.

corev1.EnvVar{Name: "WHISKER_BACKEND_UPSTREAM", Value: "linseed"},
corev1.EnvVar{
Name: "LINSEED_URL",
Value: relasticsearch.LinseedEndpoint(c.SupportedOSType(), c.cfg.ClusterDomain, render.ElasticsearchNamespace, false, false),

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.

Claude: These coordinates are fixed to the single-tenant layout, so the backend cannot work on a multi-tenant management cluster.

render.ElasticsearchNamespace is always tigera-elasticsearch. In multi-tenant mode Linseed lives in the tenant namespace, which is what render.LinseedNamespace(tenant) returns. MULTI_CLUSTER_FORWARDING_ENDPOINT below uses render.ManagerService(nil), which is likewise the single-tenant manager. LINSEED_TENANT_ID is never set, so Linseed sees an empty tenant.

The whisker controller has no Tenant at all today, so there is nothing to pass in here even if this code accepted it.

Simplest fix for this PR: make the whisker controller a no-op when opts.MultiTenant is set, paired with the gate on the manager side. Then this code is only ever reached in the single-tenant layout it already assumes.

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.

Took the simplest fix: the Enterprise whisker extension reports Disabled when opts.MultiTenant is set, so nothing is deployed on a multi-tenant management cluster, paired with the manager gate above. The single-tenant coordinates are then only reached in the layout they assume. tigera/calico-private#12086, pkg/enterprise/whisker/extension.go.

egressRules = append(egressRules, v3.Rule{
Action: v3.Allow,
Protocol: &networkpolicy.TCPProtocol,
Destination: render.DexEntityRule,

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.

Claude: This assumes a non-nil KeyValidatorConfig means Dex, but it can mean an external identity provider instead.

Authentication.spec.oidc.type: Tigera skips Dex and talks straight to the customer's own IdP. In that case eutils.GetKeyValidatorConfig returns a tigerakvc config, so KeyValidatorConfig != nil here, but OIDC_AUTH_JWKSURL is a public URL. eutils.DexEnabled() is false for that type, so the Dex CA is not added to the bundle either.

Two things then break. The whisker pod is deny-all, and its only egress is Linseed, the API server, Voltron, Dex and DNS, so the fetch of the IdP's signing keys is dropped. Even if that were allowed, the bundle is built with includeSystem: false and is mounted over /etc/pki/tls/certs, which hides the public root certificates, so TLS to the IdP would fail. The result is that every user token is rejected and the flow logs UI returns 401 for everyone.

Two options. Branch on the type: keep this rule when eutils.DexEnabled(authenticationCR), otherwise add networkpolicy.GetOIDCEgressRule(parsedIssuerURL) (the pattern used at pkg/enterprise/apiserver/extension.go:752) and build the bundle with system root certificates. Or, much cheaper for this PR: only pass KeyValidatorConfig to this component when Dex is enabled, and degrade with a clear message for type: Tigera until it is supported.

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.

Took the cheaper option. The key validator config is only built when eutils.DexEnabled(authn); an Authentication with oidc.type: Tigera is rejected with InvalidConfigf naming the reason, so the flow logs UI degrades clearly instead of returning 401 to everyone. The Dex egress rule is therefore the only one needed. tigera/calico-private#12086, pkg/enterprise/whisker/extension.go, with a test for the rejection.

toCreate = append(toCreate, c.whiskerBackendClusterRole(), c.whiskerBackendClusterRoleBinding(), c.whiskerBackendService())
if c.cfg.KeyValidatorConfig != nil {
toCreate = append(toCreate, secret.ToRuntimeObjects(c.cfg.KeyValidatorConfig.RequiredSecrets(WhiskerNamespace)...)...)
toCreate = append(toCreate, configmap.ToRuntimeObjects(c.cfg.KeyValidatorConfig.RequiredConfigMaps(WhiskerNamespace)...)...)

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.

Claude: For the external-IdP case this renders a ConfigMap the manager already owns, in the same namespace.

RequiredConfigMaps returns something only for oidc.type: Tigera, and the name is fixed: tigera-static-wellknown-jwks. WhiskerNamespace is calico-system. The manager renders that same name into that same namespace (pkg/render/manager.go:330, where ManagerNamespace is also common.CalicoNamespace).

Two controllers then write one object with different owner references, Whisker on one side and Manager on the other. Each reconcile undoes the other, so they keep rewriting it forever. Deleting either CR can also garbage-collect the ConfigMap out from under the other component.

The backend does not need it anyway. That ConfigMap exists so the manager's nginx can serve /.well-known/openid-configuration and /discovery/keys as static files to the browser, which is why the JWKSURL stored inside it is rewritten to a relative path. RequiredVolumeMounts() mounts it under /usr/share/nginx/html, and managerVolumeMounts() is used only by managerContainer(), the nginx container. whisker-backend is a Go binary that fetches the JWKS over the network via oidc.NewRemoteKeySet, and never reads those paths.

Suggest dropping RequiredConfigMaps, RequiredVolumes and RequiredVolumeMounts from this component, and keeping only RequiredEnv and RequiredAnnotations.

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.

Dropped RequiredConfigMaps, RequiredVolumes, RequiredVolumeMounts and RequiredSecrets from the whisker render; only RequiredEnv and RequiredAnnotations apply, with the reason on the Configuration field. The render test asserts no Secret or ConfigMap is rendered and the volume set is unchanged. Core side in projectcalico/calico#12879, pkg/render/whisker/component.go.

// changes. The trusted bundle is only used by a client that picks up
// changes without a restart.
annotations := map[string]string{
c.cfg.WhiskerBackendKeyPair.HashAnnotationKey(): c.cfg.WhiskerBackendKeyPair.HashAnnotationValue(),

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.

Claude: The comment above does not hold for the enterprise backend, so a CA rotation breaks it until someone deletes the pod.

These annotations cover the key pair and the key validator config, but not the trusted bundle. I checked whether the Linseed client reloads on its own, since building it once does not by itself prove it ignores later changes. It does not reload:

  • newHTTPClient (linseed/pkg/client/rest/client.go:93-137) does os.ReadFile(cfg.CACertPath) once into a fresh x509.CertPool and assigns it to tlsConfig.RootCAs. The client cert is loaded once with tls.LoadX509KeyPair.
  • There is no GetClientCertificate, GetCertificate or VerifyPeerCertificate callback, and no file watching, anywhere in linseed/pkg/client/rest/. Those callbacks are the only way a Go TLS client picks up a changed file mid-process.
  • NewClient builds that http.Client once and stores it. Nothing rebuilds the transport.
  • The same file does hot-reload where it was wanted: WithTokenPath is documented as loading the token on each request to allow rotation, and Token() re-reads it per request. The token reloads by design; the certificates do not.

So when the operator CA or tigera-secure-linseed-cert rotates, the bundle ConfigMap changes, the pod is not rolled, and every flow query then fails with an x509 error.

Suggest adding maps.Copy(annotations, c.cfg.TrustedCertBundle.HashAnnotations()). That is what the other bundle consumers do, for example pkg/render/intrusion_detection.go:1044, pkg/render/policyrecommendation.go:422 and pkg/render/guardian.go:430.

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.

Added maps.Copy(annotations, c.cfg.TrustedCertBundle.HashAnnotations()) for the backend-only deployment, and corrected the comment: whisker's own Goldmane client reloads the bundle, another flow source's client may not. A render test checks the annotation is present for backend-only and absent for the full deployment. projectcalico/calico#12879.

reqLogger.V(1).Info("Managed cluster: whisker-backend is not deployed; flow logs are served by the management cluster")
r.status.ReadyToMonitor()
r.status.ClearDegraded()
return reconcile.Result{}, nil

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.

Claude: Returning here deletes nothing, so objects from a previous configuration keep running.

The old enterprise extension did this cleanup. deleteWhisker moved everything whisker rendered onto the delete list, and extensions.Decorate gated only on the variant (pkg/extensions/component.go:32), not on the cluster type, so it covered managed clusters too. This PR removes it and nothing replaces it.

Two paths reach the problem.

  1. An Enterprise standalone or management cluster becomes managed, which is just an admin creating a ManagementClusterConnection. The backend Deployment, whisker-backend Service, ClusterRole, ClusterRoleBinding and NetworkPolicy all stay. The backend still has LINSEED_URL pointed at a Linseed that no longer serves this cluster, so it runs and fails, while TigeraStatus reports Available.
  2. An OSS managed cluster is upgraded to Enterprise. OSS managed clusters are supported (Tenant.ManagedClusterIsCalico()), and the OSS branch above ignores the ManagementClusterConnection, so whisker deploys normally there. After the upgrade the whole OSS stack survives: the Deployment with the SPA container, whisker-nginx-config, the whisker Service, and the Goldmane-egress policy.

A fresh install onto a managed cluster is fine, because there is nothing to clean up.

Two smaller things are also skipped by this return. maintainFinalizer is never called, so a render.WhiskerFinalizer added while the cluster was standalone stays on the Installation and blocks its deletion. And gwHelper.Teardown never runs.

Suggest not returning early. Render the component so everything lands on the delete list (or keep a deleteWhisker-style decorator for the managed case), and let maintainFinalizer and gwHelper.Teardown run as usual.

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.

The early return is gone. A variant now reports Disabled through RenderData and the reconcile runs on: the render puts everything any configuration could have deployed on the delete list, the controller removes the issued key pairs and the bundle ConfigMap through a deletion passthrough, and maintainFinalizer and gwHelper.Teardown run as before. A controller test does a full OSS-shaped reconcile, then disables, and asserts all eight leftovers (Deployment, Service, ConfigMap, ServiceAccount, both key pairs in both namespaces) are gone with the status not degraded. projectcalico/calico#12879.

return reconcile.Result{}, err
}
if linseedCert == nil {
r.status.SetDegraded(operatorv1.ResourceNotReady, "Linseed certificate is not available yet, waiting until it becomes available", nil, reqLogger)

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.

Claude: This is right while log storage is starting up, but it never ends when log storage is not configured at all.

Log storage is optional in Enterprise. A cluster can run with no LogStorage resource. With the Whisker CR shipping by default (tigera/calico-private#12089), tigera-secure-linseed-cert never appears on such a cluster, so every reconcile lands here. The whisker TigeraStatus then stays Degraded forever, for a perfectly valid configuration. That shows red in the UI, fires alerts, and blocks flows that wait on TigeraStatus.

Before this PR that same cluster was fine: the enterprise extension deleted whisker's objects and the status was Available.

Suggest telling the two cases apart. Read the LogStorage resource: if it is absent, render nothing and report Available; if it exists but the certificate is not ready yet, degrade as this code does now. The manager controller takes a similar approach for Kibana.

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 as you suggested: the extension reads LogStorage first. Absent means Disabled, so nothing is rendered and the status stays Available; present with the Linseed certificate not yet issued still degrades NotReady. LogStorage is watched. tigera/calico-private#12086, pkg/enterprise/whisker/extension.go, with tests for both branches.

whisker.WhiskerKeyPairSecret,
goldmane.GoldmaneKeyPairSecret,
render.TigeraLinseedSecret,
render.DexTLSSecretName,

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.

Claude: Two inputs this controller now reads are not watched, so authentication changes are picked up late.

eutils.GetKeyValidatorConfig reads the IdP secret, via GetSecretOrProviderClass and then GetIDPSecret. That is tigera-oidc-credentials, tigera-ldap-credentials or tigera-openshift-credentials in the operator namespace. A change to clientID there changes OIDC_AUTH_CLIENT_ID in the pod. None of them are watched here.

For oidc.type: Tigera the component also renders tigerakvc.StaticWellKnownJWKSConfigMapName, and that is not watched either.

The manager controller watches both, in its own secret list and at manager_controller.go:197. Without the watches, a change only takes effect on the next periodic reconcile, so logins can use stale settings until then.

Suggest adding the three IdP secret names to this list, plus an AddConfigMapWatch for tigerakvc.StaticWellKnownJWKSConfigMapName. If you also add render.ManagerInternalTLSSecretName to the trusted bundle, it will need a watch here too.

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.

Added watches for tigera-oidc-credentials, tigera-ldap-credentials and tigera-openshift-credentials, plus LogStorage. The static JWKS ConfigMap is not watched because it is no longer rendered (thread on line 172), and ManagerInternalTLSSecretName is not in the bundle so it needs no watch. tigera/calico-private#12086, Watches in pkg/enterprise/whisker/extension.go.

@hjiawei

hjiawei commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Superseded: the operator now lives in the monorepos, so this change was split and moved there.

  • The variant-blind seam (pkg/extensions/whisker.go, pkg/controller/whisker/, pkg/render/whisker/) is in Make whisker's flow source pluggable, and compress its responses projectcalico/calico#12879, and is carried patch-identical in tigera/calico-private#12086 so the pick is a no-op.
  • The Enterprise implementation (pkg/enterprise/whisker/, the manager controller, and the manager/Dex/Linseed render and policy changes) is in tigera/calico-private#12086.
  • The WhiskerSourceEntityRule helper added to the shared networkpolicy.go here was dropped; the two call sites use the existing CreateSourceEntityRule instead, so the shared file stays untouched.

Every review comment on this PR is addressed in those commits: the managed-cluster early return (teardown now runs, finalizer and gateway cleanup included), the orphaned UI key pair, the multi-tenant and managed-cluster gates on the manager, the multi-tenant backend, the Dex-only key validator with external IdPs rejected, the key validator files no longer rendered, the bundle-hash roll, the optional-LogStorage degrade, and the missing IdP secret and LogStorage watches. The hardcoded whisker-backend SAN Copilot flagged predates this PR in the core controller and was left as is.

@hjiawei hjiawei closed this Sep 4, 2026
@hjiawei
hjiawei deleted the hjiawei/whisker-commercial-flow-logs branch September 4, 2026 01:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants