From 3caa49c303f879580265190400937931a1067cef Mon Sep 17 00:00:00 2001 From: Anna Williamson Date: Tue, 16 Jun 2026 09:33:14 -0700 Subject: [PATCH] fix: gate Grafana ingress forward-auth annotation on site opt-in The Grafana Helm ingress unconditionally stamped the kube-system-traefik-forward-auth-main / -add-forwarded-headers middleware annotation. Those middlewares are only created by the clusters step when a site sets use_traefik_forward_auth: true, so on workloads that don't enable forward-auth the ingress referenced a non-existent middleware and Traefik invalidated the router (HTTP 404). Only add the annotation when the main site opts into forward-auth; otherwise Grafana routes straight through to its local-account login. Regression from the eks/cluster Python->Go migration (ae26379), which folded the previously-unconditional global traefik-forward-auth middleware into per-site, flag-gated logic. Co-Authored-By: Claude Opus 4.8 --- lib/steps/helm_aws.go | 32 +++++++++++++++----- lib/steps/helm_aws_test.go | 62 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 lib/steps/helm_aws_test.go diff --git a/lib/steps/helm_aws.go b/lib/steps/helm_aws.go index 7115739..06f53cc 100644 --- a/lib/steps/helm_aws.go +++ b/lib/steps/helm_aws.go @@ -893,6 +893,29 @@ func awsHelmLoki(ctx *pulumi.Context, k8sOpt pulumi.ResourceOption, compoundName withAlias("kubernetes:helm.cattle.io/v1:HelmChart", resourceName)) } +// grafanaIngressValues builds the Grafana Helm ingress values. +// +// Grafana is routed through traefik-forward-auth only when the main site opts into it. +// The forward-auth middlewares (kube-system-traefik-forward-auth-main and +// -add-forwarded-headers) are created by the clusters step only when a site sets +// use_traefik_forward_auth: true. Adding the annotation unconditionally leaves the +// Grafana ingress referencing a non-existent middleware on workloads that don't enable +// forward-auth, which makes Traefik invalidate the router (HTTP 404). Otherwise Grafana +// authenticates with local accounts and the ingress should route straight through. +func grafanaIngressValues(domain string, sites map[string]types.SiteConfig) map[string]interface{} { + ingress := map[string]interface{}{ + "enabled": true, + "hosts": []interface{}{fmt.Sprintf("grafana.%s", domain)}, + "path": "/", + } + if mainSite, ok := sites["main"]; ok && mainSite.Spec.UseTraefikForwardAuth { + ingress["annotations"] = map[string]interface{}{ + "traefik.ingress.kubernetes.io/router.middlewares": "kube-system-traefik-forward-auth-add-forwarded-headers@kubernetescrd,kube-system-traefik-forward-auth-main@kubernetescrd", + } + } + return ingress +} + func awsHelmGrafana(ctx *pulumi.Context, k8sOpt pulumi.ResourceOption, compoundName, release string, params awsHelmParams, version string, withAlias func(string, string) pulumi.ResourceOption) error { @@ -936,14 +959,7 @@ func awsHelmGrafana(ctx *pulumi.Context, k8sOpt pulumi.ResourceOption, compoundN values := map[string]interface{}{ "envFromSecret": "grafana-db-url", "grafana.ini": iniCfg, - "ingress": map[string]interface{}{ - "enabled": true, - "annotations": map[string]interface{}{ - "traefik.ingress.kubernetes.io/router.middlewares": "kube-system-traefik-forward-auth-add-forwarded-headers@kubernetescrd,kube-system-traefik-forward-auth-main@kubernetescrd", - }, - "hosts": []interface{}{fmt.Sprintf("grafana.%s", domain)}, - "path": "/", - }, + "ingress": grafanaIngressValues(domain, params.cfg.Sites), "datasources": map[string]interface{}{ "datasources.yaml": map[string]interface{}{ "apiVersion": 1, diff --git a/lib/steps/helm_aws_test.go b/lib/steps/helm_aws_test.go new file mode 100644 index 0000000..65fbdd3 --- /dev/null +++ b/lib/steps/helm_aws_test.go @@ -0,0 +1,62 @@ +package steps + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/posit-dev/ptd/lib/types" +) + +const grafanaForwardAuthMiddlewares = "kube-system-traefik-forward-auth-add-forwarded-headers@kubernetescrd,kube-system-traefik-forward-auth-main@kubernetescrd" + +func TestGrafanaIngressValues(t *testing.T) { + t.Run("main site opts into forward-auth adds annotation", func(t *testing.T) { + sites := map[string]types.SiteConfig{ + "main": {Spec: types.SiteConfigSpec{UseTraefikForwardAuth: true}}, + } + + ingress := grafanaIngressValues("example.com", sites) + + annotations, ok := ingress["annotations"].(map[string]interface{}) + assert.True(t, ok, "expected annotations to be present") + assert.Equal(t, grafanaForwardAuthMiddlewares, annotations["traefik.ingress.kubernetes.io/router.middlewares"]) + }) + + t.Run("main site with forward-auth disabled has no annotation", func(t *testing.T) { + sites := map[string]types.SiteConfig{ + "main": {Spec: types.SiteConfigSpec{UseTraefikForwardAuth: false}}, + } + + ingress := grafanaIngressValues("example.com", sites) + + _, hasAnnotations := ingress["annotations"] + assert.False(t, hasAnnotations, "expected no annotations when forward-auth disabled") + }) + + t.Run("no main site has no annotation", func(t *testing.T) { + sites := map[string]types.SiteConfig{ + "other": {Spec: types.SiteConfigSpec{UseTraefikForwardAuth: true}}, + } + + ingress := grafanaIngressValues("example.com", sites) + + _, hasAnnotations := ingress["annotations"] + assert.False(t, hasAnnotations, "expected no annotations when no main site is present") + }) + + t.Run("nil sites map has no annotation", func(t *testing.T) { + ingress := grafanaIngressValues("example.com", nil) + + _, hasAnnotations := ingress["annotations"] + assert.False(t, hasAnnotations, "expected no annotations when sites map is nil") + }) + + t.Run("basic ingress values", func(t *testing.T) { + ingress := grafanaIngressValues("example.com", nil) + + assert.Equal(t, true, ingress["enabled"]) + assert.Equal(t, []interface{}{"grafana.example.com"}, ingress["hosts"]) + assert.Equal(t, "/", ingress["path"]) + }) +}