Group 3 of the #114 sweep. One route, one rule, at the root - and THREE EXPECTATIONS ABOUT THIS CHART DID NOT SURVIVE CONTACT WITH IT: - #114 says bulwark has TWO Ingress objects. It has ONE. keycloak has two. - #114's measured table lists bulwark under rewrite-target. It has no rewrite-target annotation and CANNOT have one: NEXT_PUBLIC_BASE_PATH is compiled into the bundle, so the application cannot be served under a sub-path, and stripping a prefix at the proxy would yield a page that loads and then fetches everything from the wrong place. Every occurrence of the string "rewrite-target" in this chart - values.yaml, the schema, the Ingress template - is an explanation of exactly that. A grep for the string finds the refutations and counts them as occurrences. - ci/run-httproute-match-test.sh said in its header that bulwark was the second rewrite-target chart and would meet the #118 prefix/redirect assurance when group 3 landed. Group 3 landed and it does not: with no URLRewrite on a prefix other than "/", the assurance's precondition is never met here. The header is corrected rather than left standing, and it now says freesailarr is the only chart the pairing applies to. So no redirect rule is added, and none is needed. WHAT THIS MODE DROPS, MEASURED ON THE LIVE OBJECT rather than inferred. On 2026-10-12 the live Ingress on webmail.bekemeyer-klein.de carried nginx.ingress.kubernetes.io/proxy-body-size: 25m supplied by the OPERATOR through ingress.annotations - values.yaml recommends exactly that value and explains why the chart cannot pick it, so the chart sets nothing and a reader of this repository alone would not find it. It is the single annotation this installation depends on, and the Gateway API has no body-size counterpart: implementation policy, not routing. Per decisions 1 and 2 of #114 it is NAMED, not rebuilt - in values.yaml, in the schema and in the Gateway branch of NOTES.txt, in this application's own terms: an attachment above the Gateway's default is rejected before Bulwark sees a byte, the 413 comes from the proxy, and it arrives after the user has written the mail and attached the file. CORRECTED CLAIM, the same one garage and calibre-web-automated needed: the Ingress template, the bulwark.publicUrl helper and the schema all stated that nothing outside the Ingress template FUNCTIONALLY reads ingress.domain. The HTTPRoute reads it, so the claim held only while both modes were off. All three now say so, and the route carries a required() of its own. bulwark.publicUrl follows the ACTIVE mode instead of always printing ingress.domain. The two differ exactly when httpRoute.hostnames is set, which is how a migration off the Ingress runs - and printing the old host there would hand the operator an OIDC redirect URI for the hostname being migrated away from, a failure that surfaces as a rejected login at the identity provider and says nothing about this chart. Minor: additive, defaults unchanged. Empty helm template diff against origin/main with ci/bulwark/test-values.yaml. Part of #114
482 lines
21 KiB
Smarty
482 lines
21 KiB
Smarty
{{/*
|
|
Render the body of a container "resources:" block from a values dict of the form
|
|
|
|
resources:
|
|
limitFactor: 3 # optional, default 3
|
|
requests: { cpu, memory, ephemeral-storage / ephemeralStorage, ... }
|
|
limits: { ... } # optional, every entry optional
|
|
|
|
Rules (variant B, see issue #19):
|
|
- requests are rendered as given (camelCase "ephemeralStorage" is normalized
|
|
to the Kubernetes key "ephemeral-storage").
|
|
- an explicitly set limit always wins.
|
|
- a missing memory / ephemeral-storage limit is computed as
|
|
limitFactor x the corresponding request.
|
|
- a missing cpu limit is NOT set at all (the kubelet then treats CPU as
|
|
unlimited; CPU is compressible, so this cannot kill the workload).
|
|
|
|
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
|
the chart's name as define prefix, because sharing it would mean a library
|
|
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
|
this chart takes none. The reference implementation is
|
|
template-chart/templates/_helpers.tpl.
|
|
*/}}
|
|
{{- define "bulwark.resources" -}}
|
|
{{- $resources := . | default dict -}}
|
|
{{- $factor := 3.0 -}}
|
|
{{- if not (kindIs "invalid" $resources.limitFactor) -}}
|
|
{{- $factor = float64 $resources.limitFactor -}}
|
|
{{- end -}}
|
|
{{- $requests := dict -}}
|
|
{{- range $key, $value := ($resources.requests | default dict) -}}
|
|
{{- if not (kindIs "invalid" $value) -}}
|
|
{{- $_ := set $requests (include "bulwark.resourceKey" $key) $value -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- $limits := dict -}}
|
|
{{- range $key, $value := ($resources.limits | default dict) -}}
|
|
{{- if not (kindIs "invalid" $value) -}}
|
|
{{- $_ := set $limits (include "bulwark.resourceKey" $key) $value -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- range $key, $request := $requests -}}
|
|
{{- if and (ne $key "cpu") (not (hasKey $limits $key)) -}}
|
|
{{- $_ := set $limits $key (include "bulwark.multiplyQuantity" (dict "quantity" $request "factor" $factor)) -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- $out := dict -}}
|
|
{{- with $requests }}{{- $_ := set $out "requests" . }}{{- end -}}
|
|
{{- with $limits }}{{- $_ := set $out "limits" . }}{{- end -}}
|
|
{{- toYaml $out -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Normalize a resource name from values to its Kubernetes key.
|
|
*/}}
|
|
{{- define "bulwark.resourceKey" -}}
|
|
{{- if eq . "ephemeralStorage" -}}ephemeral-storage{{- else -}}{{ . }}{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Multiply a Kubernetes quantity by a factor, keeping its suffix.
|
|
Input: dict "quantity" <string|number> "factor" <float64>.
|
|
*/}}
|
|
{{- define "bulwark.multiplyQuantity" -}}
|
|
{{- $quantity := include "bulwark.formatNumber" .quantity -}}
|
|
{{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}}
|
|
{{- if not $number -}}
|
|
{{- fail (printf "cannot parse resource quantity %q" $quantity) -}}
|
|
{{- end -}}
|
|
{{- $suffix := trimPrefix $number $quantity -}}
|
|
{{- if not (regexMatch "^(m|k|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$" $suffix) -}}
|
|
{{- fail (printf "unsupported resource quantity suffix %q in %q" $suffix $quantity) -}}
|
|
{{- end -}}
|
|
{{- $product := mulf (float64 $number) .factor -}}
|
|
{{- if eq $product (floor $product) -}}
|
|
{{- printf "%d%s" (int64 $product) $suffix -}}
|
|
{{- else -}}
|
|
{{- printf "%s%s" (include "bulwark.formatNumber" $product) $suffix -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Render a number as a plain decimal string, never in exponential notation
|
|
(issue #157): the null-safe fallback helpers pass values through fromYaml,
|
|
which parses every number as float64, and Go's "%v" switches float64 to
|
|
exponential notation from about 1e6 upwards - "1.048576e+06" is not a valid
|
|
Kubernetes quantity. Strings ("512Mi") are passed through untouched.
|
|
*/}}
|
|
{{- define "bulwark.formatNumber" -}}
|
|
{{- if kindIs "float64" . -}}
|
|
{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}}
|
|
{{- else -}}
|
|
{{- printf "%v" . -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Render the replica count for the StatefulSet.
|
|
|
|
Input: dict "scaleDown" <flag|nil> "replicas" <value|nil>.
|
|
|
|
- scaleDown: true takes precedence and always yields 0.
|
|
- otherwise an explicitly set replicas value is used as given, including an
|
|
explicit 0 - Helm's "default" would swallow 0, hence the kindIs "invalid"
|
|
nil check.
|
|
- a missing replicas value defaults to 1.
|
|
|
|
A NOTE SPECIFIC TO THIS CHART: the schema caps this at 1, and the reason is the
|
|
single ReadWriteOnce PVC rather than anything about the application. Two pods
|
|
cannot both mount it, so a second replica would sit in ContainerCreating
|
|
forever - and the PVC is not optional here, because /app/data carries the admin
|
|
configuration, the audit log and the synchronized user settings. An RWX volume
|
|
would not make it a cluster either: nothing in Bulwark coordinates two
|
|
processes over that directory.
|
|
*/}}
|
|
{{- define "bulwark.replicas" -}}
|
|
{{- if .scaleDown -}}
|
|
0
|
|
{{- else if not (kindIs "invalid" .replicas) -}}
|
|
{{- .replicas -}}
|
|
{{- else -}}
|
|
1
|
|
{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Merge a user-supplied values sub-tree onto a dict of chart defaults so that a
|
|
null / empty override falls back to the defaults instead of erasing them
|
|
(issues #61, #99). Input: dict "defaults" <dict> "given" <dict|nil> "path"
|
|
<string>. Output: YAML of the merged dict (consume it with fromYaml).
|
|
|
|
An override key written without children is YAML null, and Helm treats that
|
|
null as a deletion of the chart's default. This helper defines the opposite
|
|
semantics - an empty block means "chart defaults apply" - while an explicitly
|
|
set value still wins, including false and 0.
|
|
*/}}
|
|
{{- define "bulwark.defaultedDict" -}}
|
|
{{- $defaults := .defaults | default dict -}}
|
|
{{- $given := .given -}}
|
|
{{- $path := .path | default "value" -}}
|
|
{{- if kindIs "invalid" $given -}}
|
|
{{- $given = dict -}}
|
|
{{- end -}}
|
|
{{- if not (kindIs "map" $given) -}}
|
|
{{- fail (printf "%s must be a mapping or null, got %s (%v)" $path (kindOf $given) $given) -}}
|
|
{{- end -}}
|
|
{{- $out := deepCopy $defaults -}}
|
|
{{- range $key, $value := $given -}}
|
|
{{- if not (kindIs "invalid" $value) -}}
|
|
{{- $default := index $defaults $key -}}
|
|
{{- if and (kindIs "map" $value) (kindIs "map" $default) -}}
|
|
{{- $_ := set $out $key (fromYaml (include "bulwark.defaultedDict" (dict "defaults" $default "given" $value "path" (printf "%s.%s" $path $key)))) -}}
|
|
{{- else if kindIs "map" $default -}}
|
|
{{- fail (printf "%s.%s must be a mapping or null, got %s (%v)" $path $key (kindOf $value) $value) -}}
|
|
{{- else -}}
|
|
{{- $_ := set $out $key $value -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
{{- toYaml $out -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Pick one sub-block out of a values block that may itself be null.
|
|
Input: dict "block" <dict|nil> "key" <string> "path" <string>.
|
|
Returns YAML of dict "value" <the sub-block, may be nil>.
|
|
*/}}
|
|
{{- define "bulwark.subBlock" -}}
|
|
{{- $block := .block -}}
|
|
{{- if kindIs "invalid" $block -}}
|
|
{{- $block = dict -}}
|
|
{{- end -}}
|
|
{{- if not (kindIs "map" $block) -}}
|
|
{{- fail (printf "%s must be a mapping or null, got %s (%v)" .path (kindOf $block) $block) -}}
|
|
{{- end -}}
|
|
{{- toYaml (dict "value" (index $block .key)) -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
THE EFFECTIVE BULWARK SETTINGS - everything the StatefulSet, the Service and
|
|
NOTES.txt read out of .Values.bulwark, merged onto this chart's defaults.
|
|
|
|
Why the whole settings block goes through the null-safe helper and not only the
|
|
three usual suspects (guideline 12.2: "the rule is about the shape, not about
|
|
those three block kinds"): every value below is dereferenced by a template, and
|
|
an erased one would not abort the render. An erased bulwark.port renders an
|
|
empty containerPort and an empty Service targetPort - rejected by nothing until
|
|
the API server sees it; an erased bulwark.oauth.enabled would silently turn OIDC
|
|
OFF on a release whose whole login path is OIDC, which is the quiet half and the
|
|
worse one. Satisfactory shipped exactly this shape with half its blocks
|
|
null-safe and half not (Gitea #16).
|
|
|
|
The defaults here MUST stay in sync with values.yaml, which remains the
|
|
documented, user-facing place for them; this copy is only the fallback.
|
|
|
|
Usage: {{ $settings := fromYaml (include "bulwark.settings" .) }}
|
|
*/}}
|
|
{{- define "bulwark.settings" -}}
|
|
{{- $given := (fromYaml (include "bulwark.subBlock" (dict "block" .Values "key" "bulwark" "path" ".Values"))).value -}}
|
|
{{- $defaults := dict
|
|
"replicas" 1
|
|
"port" 3000
|
|
"jmapServerUrl" nil
|
|
"appName" nil
|
|
"stalwartFeatures" true
|
|
"stalwartAdminAccess" "auto"
|
|
"settingsSync" true
|
|
"adminConfigReadOnly" false
|
|
"telemetry" false
|
|
"updateCheck" false
|
|
"logLevel" "info"
|
|
"logFormat" "json"
|
|
"writableCache" (dict
|
|
"tmpSizeLimit" "256Mi"
|
|
"nextCacheSizeLimit" "512Mi")
|
|
"oauth" (dict
|
|
"enabled" true
|
|
"only" true
|
|
"clientId" nil
|
|
"issuerUrl" nil
|
|
"scopes" nil
|
|
"endSession" true) -}}
|
|
{{- include "bulwark.defaultedDict" (dict "defaults" $defaults "given" $given "path" "bulwark") -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
THE DATA DIRECTORY, defined once because two templates have to agree on it: the
|
|
StatefulSet mounts the PVC here and NOTES.txt names it.
|
|
|
|
/app/data, and it is NOT a value. Bulwark resolves its four directories
|
|
relative to the process working directory - getConfigDir() falls back to
|
|
<cwd>/data/admin and getStateDir() to <cwd>/data/admin-state (lib/admin/paths.ts
|
|
at tag 1.12.0) - and cwd is /app (the image's WORKDIR). A path that disagreed
|
|
with the mount would put the admin configuration and the audit log into the
|
|
container's ephemeral layer, which looks like it works until the pod is
|
|
replaced. The image creates all four subdirectories and chowns them to
|
|
1001:1001 (Dockerfile: mkdir -p /app/data/{settings,admin,admin-state,telemetry}
|
|
&& chown -R nextjs:nodejs /app/data) - but that happens in the IMAGE LAYER,
|
|
which the PVC mount covers, so the subdirectories are created again at runtime
|
|
by Bulwark itself (ensureConfigDir / ensureStateDir, both mkdir -p). fsGroup is
|
|
what makes the empty volume writable for uid 1001.
|
|
*/}}
|
|
{{- define "bulwark.dataDir" -}}/app/data{{- end -}}
|
|
|
|
{{/*
|
|
WHERE THE TWO FILE-FORM SECRETS ARE MOUNTED, and the one property of this mount
|
|
that must not be "tidied up": it is a WHOLE-DIRECTORY mount with NO subPath.
|
|
|
|
The kubelet never refreshes a subPath mount. Both files are re-read on every
|
|
use - getSessionSecret() calls readFileEnv on every call
|
|
(lib/auth/session-secret.ts) and getGlobalClientSecret() does the same on every
|
|
token exchange (lib/oauth/token-exchange.ts, no cache) - so rotating a field
|
|
inside the vault item takes effect WITHOUT a pod restart. With a subPath that
|
|
property is silently lost: the file in the container would keep the old value
|
|
forever while the vault, the Secret and `helm upgrade` all look correct.
|
|
*/}}
|
|
{{- define "bulwark.secretMountPath" -}}/etc/bulwark/secrets{{- end -}}
|
|
|
|
{{/*
|
|
The effective security contexts, probes and resources (issue #99).
|
|
|
|
The defaults below MUST stay in sync with values.yaml. They reproduce THIS
|
|
chart's defaults, not the repo standard.
|
|
*/}}
|
|
{{- define "bulwark.podSecurityContext" -}}
|
|
{{- $given := (fromYaml (include "bulwark.subBlock" (dict "block" . "key" "pod" "path" "bulwark.securityContext"))).value -}}
|
|
{{- $defaults := dict
|
|
"runAsNonRoot" true
|
|
"runAsUser" 1001
|
|
"runAsGroup" 1001
|
|
"fsGroup" 1001
|
|
"fsGroupChangePolicy" "Always"
|
|
"seccompProfile" (dict "type" "RuntimeDefault") -}}
|
|
{{- include "bulwark.defaultedDict" (dict "defaults" $defaults "given" $given "path" "bulwark.securityContext.pod") -}}
|
|
{{- end -}}
|
|
|
|
{{- define "bulwark.containerSecurityContext" -}}
|
|
{{- $given := (fromYaml (include "bulwark.subBlock" (dict "block" . "key" "container" "path" "bulwark.securityContext"))).value -}}
|
|
{{- $defaults := dict
|
|
"allowPrivilegeEscalation" false
|
|
"readOnlyRootFilesystem" true
|
|
"runAsNonRoot" true
|
|
"capabilities" (dict "drop" (list "ALL") "add" (list)) -}}
|
|
{{- include "bulwark.defaultedDict" (dict "defaults" $defaults "given" $given "path" "bulwark.securityContext.container") -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
ALL THREE PROBES ARE AN HTTP GET ON /api/health, AND WHAT THAT DOES AND DOES NOT
|
|
CARRY IS MEASURED AGAINST THE SOURCE AT TAG 1.12.0 - not read off the path name,
|
|
which is the mistake #218 and Gitea #83 each cost this repo once.
|
|
|
|
FIRST, WHY GET AND NOT HEAD, because the plan for this chart (Gitea #89) said
|
|
HEAD and HEAD IS NOT AVAILABLE TO A KUBERNETES PROBE AT ALL. The API type is
|
|
HTTPGetAction and it has no method field; the kubelet's prober issues a GET and
|
|
there is no value anywhere in a pod spec that changes that. The only way to send
|
|
a HEAD would be an exec probe shelling out to a HTTP client - and this image
|
|
contains no curl and no wget (npm and npx are deliberately removed from the
|
|
runtime layer), so it would mean `node -e` on every probe tick: a new Node
|
|
process every few seconds against an endpoint whose GET handler is already
|
|
cheap. That is strictly worse, so the chart uses the GET that Kubernetes
|
|
actually offers.
|
|
|
|
WHAT CHANGES BECAUSE OF THAT: almost nothing, and in the direction that helps.
|
|
Upstream's HEAD handler returns `new Response(null, { status: 200 })` and
|
|
computes nothing. The GET handler does gather diagnostics, which makes it very
|
|
slightly more capable than HEAD rather than less: its catch block answers 503
|
|
when `process.memoryUsage()` or `v8.getHeapStatistics()` itself throws. Every
|
|
limitation argued below is unchanged.
|
|
|
|
WHAT THE ENDPOINT DOES. The GET success path returns a hard `status: 200`
|
|
whatever state it computed, and for an UNAUTHENTICATED caller the body is
|
|
hard-coded to `healthy` as well (`status: detailed ? status : 'healthy'`);
|
|
`degraded` appears only with `?detailed=true` AND a caller that passes
|
|
canSeeInstanceDetails. Upstream tests exactly that -
|
|
lib/__tests__/health-route.test.ts expects HTTP 200 while `payload.status` is
|
|
`degraded`. A probe is unauthenticated and asks for no details, so it sees 200
|
|
and "healthy" always.
|
|
|
|
SO THE PROBE CANNOT REPORT THE APPLICATION'S STATE - and this is deliberately
|
|
NOT filed as "a probe that cannot fail", because that would be wrong and would
|
|
invite somebody to replace it with something worse. What a 200 here really
|
|
proves, all of it load-bearing: the Node process is alive, the HTTP server is
|
|
accepting, the event loop is not blocked, and Next.js can route a request. An
|
|
OOM kill, a crash, a wedged event loop and a container that never finished
|
|
booting all fail this probe. That is more than #218's endpoint carried and it is
|
|
exactly what a liveness and a startup probe are for.
|
|
|
|
WHAT IT DOES NOT CARRY, and no probe in this chart can: anything about
|
|
STALWART. Bulwark holds no mail and this endpoint does not ask the JMAP server
|
|
anything, so with the mail server gone /api/health is green and the pod stays
|
|
Ready. There is no better endpoint - /api/setup/test-jmap would really test the
|
|
connection, but it lives behind the bootstrap setup token and is unreachable in
|
|
this chart (the wizard is disabled by jmapServerUrl). The limit is written at
|
|
the readiness value in values.yaml because readiness is the probe whose promise
|
|
it weakens.
|
|
*/}}
|
|
{{- define "bulwark.livenessProbe" -}}
|
|
{{- $defaults := dict
|
|
"httpGet" (dict "path" "/api/health" "port" "http" "scheme" "HTTP")
|
|
"periodSeconds" 10
|
|
"timeoutSeconds" 5
|
|
"failureThreshold" 3 -}}
|
|
{{- include "bulwark.defaultedDict" (dict "defaults" $defaults "given" . "path" "bulwark.livenessProbe") -}}
|
|
{{- end -}}
|
|
|
|
{{- define "bulwark.readinessProbe" -}}
|
|
{{- $defaults := dict
|
|
"httpGet" (dict "path" "/api/health" "port" "http" "scheme" "HTTP")
|
|
"periodSeconds" 10
|
|
"timeoutSeconds" 5
|
|
"failureThreshold" 3 -}}
|
|
{{- include "bulwark.defaultedDict" (dict "defaults" $defaults "given" . "path" "bulwark.readinessProbe") -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
STARTUP: the same endpoint, and it gates the other two - which is why neither of
|
|
them carries an initialDelaySeconds.
|
|
|
|
BUDGET = periodSeconds x failureThreshold = 5 x 24 = 120s. What it has to cover
|
|
is not Bulwark's own boot, which is a Next.js standalone server starting in
|
|
about a second: it is attaching and mounting the network volume, and the
|
|
instrumentation hook that runs BEFORE the first request is served -
|
|
instrumentation.node.ts migrates the legacy admin layout, loads config.json and
|
|
policy.json off the volume, and on a first install hashes ADMIN_PASSWORD with
|
|
scrypt and writes admin.json. The scrypt hash is the one deliberately slow step
|
|
in there.
|
|
*/}}
|
|
{{- define "bulwark.startupProbe" -}}
|
|
{{- $defaults := dict
|
|
"httpGet" (dict "path" "/api/health" "port" "http" "scheme" "HTTP")
|
|
"periodSeconds" 5
|
|
"timeoutSeconds" 5
|
|
"failureThreshold" 24 -}}
|
|
{{- include "bulwark.defaultedDict" (dict "defaults" $defaults "given" . "path" "bulwark.startupProbe") -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
The effective resources block, fed into the resources helper above so that an
|
|
erased "resources:" still yields the chart's requests and computed limits.
|
|
*/}}
|
|
{{- define "bulwark.effectiveResources" -}}
|
|
{{- $defaults := dict
|
|
"limitFactor" 3
|
|
"requests" (dict "cpu" "100m" "memory" "256Mi" "ephemeral-storage" "64Mi") -}}
|
|
{{- $merged := fromYaml (include "bulwark.defaultedDict" (dict "defaults" $defaults "given" . "path" "bulwark.resources")) -}}
|
|
{{- include "bulwark.resources" $merged -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
The additive env block, rendered through tpl with the value, secretKeyRef and
|
|
configMapKeyRef forms (guideline 10).
|
|
|
|
ADDITIVE MEANS ADDITIVE: an entry repeating a name this chart already sets does
|
|
not replace it. At install time both reach the pod and the later one wins; on an
|
|
EXISTING release the pair collapses to the chart's own value, because Kubernetes
|
|
merges env by name - and helm upgrade still reports success. So do not use this
|
|
block to override JMAP_SERVER_URL, OAUTH_ONLY or SESSION_SECRET_FILE; change the
|
|
values that produce them.
|
|
|
|
AND THERE IS A SECOND, SHARPER REASON NOT TO USE IT FOR BULWARK'S OWN SETTINGS,
|
|
specific to this application: an environment variable is only the MIDDLE layer
|
|
of three. configManager.get resolves "admin override > env var > default"
|
|
(lib/admin/config-manager.ts), so any key the admin dashboard has written into
|
|
config.json on the PVC beats whatever arrives here - silently. An entry added
|
|
through this block is therefore not a guarantee, it is a default that the volume
|
|
may already be overriding. values.yaml, under bulwark.adminConfigReadOnly, has
|
|
the whole precedence story.
|
|
*/}}
|
|
{{- define "bulwark.env" -}}
|
|
{{- $root := .root -}}
|
|
{{- range $value := .env }}
|
|
{{- if $value.value }}
|
|
- name: {{ $value.name | quote }}
|
|
# quote is required, not cosmetic: a value like "3000" or "true" renders
|
|
# unquoted as a YAML number/bool, and the API server then rejects the whole
|
|
# manifest with "cannot unmarshal number into Go struct field EnvVar.value of
|
|
# type string".
|
|
value: {{ tpl $value.value $root | quote }}
|
|
{{- else if $value.valueFrom }}
|
|
{{- if $value.valueFrom.secretKeyRef }}
|
|
- name: {{ $value.name }}
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: {{ tpl $value.valueFrom.secretKeyRef.name $root | quote }}
|
|
key: {{ tpl $value.valueFrom.secretKeyRef.key $root | quote }}
|
|
{{- else if $value.valueFrom.configMapKeyRef }}
|
|
- name: {{ $value.name }}
|
|
valueFrom:
|
|
configMapKeyRef:
|
|
name: {{ tpl $value.valueFrom.configMapKeyRef.name $root | quote }}
|
|
key: {{ tpl $value.valueFrom.configMapKeyRef.key $root | quote }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
The public base URL, derived from ingress.domain. NOTES.txt prints it and builds
|
|
the OIDC redirect URI from it, which is why it is a helper rather than several
|
|
copies of the same printf.
|
|
|
|
DELIBERATELY NO required() HERE, and that is the opposite of what it first looks
|
|
like it should have. The only caller is NOTES.txt, and `helm template` does not
|
|
render NOTES.txt (README, guideline 12.2) - so a required() in this helper would
|
|
be a guard that cannot fire in lint, in the render test or in CI, and would fire
|
|
for the first time during a real `helm install`. A guard that only runs in
|
|
production is worse than no guard.
|
|
|
|
The honest position for THIS chart, checked in both directions as guideline 12.1
|
|
asks, AND CORRECTED WITH THE GATEWAY-API SUPPORT (Gitea #114): it used to say
|
|
that nothing outside the Ingress template FUNCTIONALLY reads ingress.domain.
|
|
That was true and is no longer - bulwark.httproute.yaml reads it as the route's
|
|
hostname, with a required() of its own. What still holds is the half that made
|
|
the claim worth making: Bulwark derives its own public URL from the request
|
|
origin and takes no base-URL variable, so with BOTH modes off the release is
|
|
complete without a domain - the calibre-web-automated case, where the gate
|
|
frees both values. The enforcement sits in whichever routing template is
|
|
active; this helper still renders a visible placeholder instead of failing,
|
|
because `helm template` does not render NOTES.txt.
|
|
|
|
IN GATEWAY MODE THE PUBLIC URL IS THE ROUTE'S HOSTNAME, not ingress.domain, and
|
|
the two differ exactly when httpRoute.hostnames is set explicitly - which is
|
|
how a migration off the Ingress runs. Printing ingress.domain there would hand
|
|
the operator an OIDC redirect URI for the host being migrated AWAY from, and
|
|
that failure surfaces as a rejected login at the identity provider rather than
|
|
as anything about this chart. So the helper follows the active mode.
|
|
*/}}
|
|
{{- define "bulwark.publicUrl" -}}
|
|
{{- $httpRoute := .Values.httpRoute | default dict -}}
|
|
{{- $host := .Values.ingress.domain -}}
|
|
{{- if $httpRoute.enabled -}}
|
|
{{- $host = (first ($httpRoute.hostnames | default list)) | default .Values.ingress.domain -}}
|
|
{{- end -}}
|
|
{{- if $host -}}
|
|
https://{{ $host }}
|
|
{{- else if $httpRoute.enabled -}}
|
|
https://<neither httpRoute.hostnames nor ingress.domain SET>
|
|
{{- else -}}
|
|
https://<ingress.domain NOT SET>
|
|
{{- end -}}
|
|
{{- end -}}
|