Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions helm/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,81 @@ func TestDeploymentOIDCProvidersSecretConflictsWithLiteral(t *testing.T) {
}
}

func TestDeploymentZitadelLinkSecretConflictsWithLiteral(t *testing.T) {
helmPath := requireHelm(t)

for _, key := range []string{"ZITADEL_LINK_CLIENT_ID", "ZITADEL_LINK_CLIENT_SECRET"} {
// #nosec G204 -- helmPath is resolved via exec.LookPath("helm"), and the
// arguments are fixed literals.
cmd := exec.Command(helmPath, "template", "mctl-api", ".",
"--set", "zitadelLinkSecret=mctl-api-oidc-zitadel",
"--set-string", "env."+key+"=literal",
)
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
if err := cmd.Run(); err == nil {
t.Fatalf("expected helm template to fail when both env.%s and zitadelLinkSecret are set", key)
}
if !strings.Contains(out.String(), "not both") {
t.Errorf("%s: unexpected failure output:\n%s", key, out.String())
}
}
}

// An empty literal does not trip the guard, but must not render a second
// entry next to the secretKeyRef either.
func TestDeploymentZitadelLinkSecretSkipsAnEmptyLiteral(t *testing.T) {
d, _ := renderDeployment(t, "--set", "zitadelLinkSecret=mctl-api-oidc-zitadel",
"--set-string", "env.ZITADEL_LINK_CLIENT_ID=")
n := 0
for _, e := range d.Spec.Template.Spec.Containers[0].Env {
if e.Name == "ZITADEL_LINK_CLIENT_ID" {
n++
if e.ValueFrom == nil || e.ValueFrom.SecretKeyRef == nil {
t.Errorf("ZITADEL_LINK_CLIENT_ID rendered as a literal: %+v", e)
}
}
}
if n != 1 {
t.Errorf("ZITADEL_LINK_CLIENT_ID rendered %d times, want 1", n)
}
}

func TestDeploymentEmptyZitadelLinkSecretRendersUnchanged(t *testing.T) {
_, defaultOut := renderDeployment(t)
_, emptyOut := renderDeployment(t, "--set", "zitadelLinkSecret=")

if !bytes.Equal(defaultOut, emptyOut) {
t.Error("empty zitadelLinkSecret should render byte-identical output to the default")
}
}

func TestDeploymentRendersZitadelLinkSecretEnv(t *testing.T) {
d, _ := renderDeployment(t, "--set", "zitadelLinkSecret=mctl-api-oidc-zitadel")
c := mainContainer(t, d)

for _, name := range []string{"ZITADEL_LINK_CLIENT_ID", "ZITADEL_LINK_CLIENT_SECRET"} {
if n := countEnv(c, name); n != 1 {
t.Fatalf("expected exactly one %s env entry, got %d", name, n)
}
env, _ := findEnv(c, name)
if env.Value != "" {
t.Errorf("%s.value = %q, want empty (no literal in the manifest)", name, env.Value)
}
if env.ValueFrom == nil || env.ValueFrom.SecretKeyRef == nil {
t.Fatalf("expected %s to be sourced from a secretKeyRef", name)
}
ref := env.ValueFrom.SecretKeyRef
if ref.Name != "mctl-api-oidc-zitadel" || ref.Key != name {
t.Errorf("%s secretKeyRef = %s/%s, want mctl-api-oidc-zitadel/%s", name, ref.Name, ref.Key, name)
}
if !ref.Optional {
t.Errorf("expected %s secretKeyRef.optional to be true", name)
}
}
}

func TestDeploymentUsagePricingKeyOverride(t *testing.T) {
d, _ := renderDeployment(t,
"--set", "usagePricingConfigMap=cm",
Expand Down Expand Up @@ -470,6 +545,7 @@ func TestHelmLintCleanAcrossOptionalValues(t *testing.T) {
{"--set", "usagePricingConfigMap=mctl-api-usage-pricing"},
{"--set", "surfaceTelegramTokenSecret=mctl-api-surface-telegram"},
{"--set", "oidcProvidersSecret=mctl-api-oidc-zitadel"},
{"--set", "zitadelLinkSecret=mctl-api-oidc-zitadel"},
}
for _, extra := range cases {
args := append([]string{"lint", "."}, extra...)
Expand Down
31 changes: 27 additions & 4 deletions helm/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,16 @@ spec:
{{- if and .Values.oidcProvidersSecret .Values.env.MCTL_OIDC_PROVIDERS }}
{{- fail "set MCTL_OIDC_PROVIDERS either in env or through oidcProvidersSecret, not both" }}
{{- end }}
{{- if and .Values.zitadelLinkSecret (or .Values.env.ZITADEL_LINK_CLIENT_ID .Values.env.ZITADEL_LINK_CLIENT_SECRET) }}
{{- fail "set ZITADEL_LINK_CLIENT_ID and ZITADEL_LINK_CLIENT_SECRET either in env or through zitadelLinkSecret, not both" }}
{{- end }}
{{- range $key, $value := .Values.env }}
{{- /* oidcProvidersSecret supplies it below; the empty default in
values.yaml would otherwise render a second entry with the
same name, which server-side apply refuses. */}}
{{- if not (and $.Values.oidcProvidersSecret (eq $key "MCTL_OIDC_PROVIDERS")) }}
{{- /* oidcProvidersSecret and zitadelLinkSecret supply these
below; a literal entry (even an empty one) would otherwise
render a second entry with the same name, which
server-side apply refuses. */}}
{{- if not (or (and $.Values.oidcProvidersSecret (eq $key "MCTL_OIDC_PROVIDERS"))
(and $.Values.zitadelLinkSecret (or (eq $key "ZITADEL_LINK_CLIENT_ID") (eq $key "ZITADEL_LINK_CLIENT_SECRET")))) }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
Expand Down Expand Up @@ -120,6 +125,24 @@ spec:
key: MCTL_OIDC_PROVIDERS
optional: true
{{- end }}
{{- if .Values.zitadelLinkSecret }}
Comment thread
mashkovd marked this conversation as resolved.
# The confidential ZITADEL client of identity linking
# (docs/principals.md, mctlhq/mctl-api#435), written by the
# zitadel-iac Job. Optional: without both keys the link flow
# answers 503 and nothing else changes.
- name: ZITADEL_LINK_CLIENT_ID
valueFrom:
secretKeyRef:
name: {{ .Values.zitadelLinkSecret }}
key: ZITADEL_LINK_CLIENT_ID
optional: true
- name: ZITADEL_LINK_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: {{ .Values.zitadelLinkSecret }}
key: ZITADEL_LINK_CLIENT_SECRET
optional: true
{{- end }}
{{- if .Values.usagePricingConfigMap }}
# The path, not the JSON. internal/usage/catalog_file.go reads this file
# with os.ReadFile; a rate card is reviewed as a file in GitOps, and an env
Expand Down
8 changes: 8 additions & 0 deletions helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ surfacePortalTokenSecret: ""
# the Dex-only configuration, unchanged.
oidcProvidersSecret: ""

# Optional: Secret holding ZITADEL_LINK_CLIENT_ID and
# ZITADEL_LINK_CLIENT_SECRET, the confidential ZITADEL client of explicit
# identity linking (docs/principals.md, mctlhq/mctl-api#435). In production
# the zitadel-iac Job writes both into the same Secret as
# MCTL_OIDC_PROVIDERS. Optional, so absent keys leave the link flow off
# (503) rather than failing the pod.
zitadelLinkSecret: ""

# Optional: mount a versioned usage rate card from a ConfigMap.
#
# When set, the ConfigMap is mounted read-only at
Expand Down
Loading