Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 13s
Validate Helm Charts / Build bulwark (pull_request) Successful in 6s
Validate Helm Charts / Trivy config bulwark (pull_request) Successful in 10s
Install-test / Install-test bulwark (pull_request) Successful in 8m50s
Validate Helm Charts / Install-test (pull_request) Successful in 8m50s
A JMAP webmail client for the stalwart chart (Gitea #65): StatefulSet, one PVC at /app/data, Service plus headless Service, Ingress at a host root, two OnePasswordItems, three probes, computed limits, replicas plus scaleDown, strict values.schema.json. Everything below was measured against the source at tag 1.12.0 rather than read out of the documentation, which was wrong about three of them. THE DEFINING PROPERTY: THE VOLUME BEATS THE CHART, FOR EVERY VALUE, SILENTLY. configManager.get resolves "admin override > env var > default", the admin dashboard may write any key of CONFIG_ENV_MAP, and nothing reports that it just disconnected the chart - so a helm upgrade can succeed and change nothing. Upstream documents this for the server URL only; it is true of everything. Exactly three values escape it (SESSION_SECRET(_FILE), ADMIN_PASSWORD, BULWARK_TELEMETRY). The value that closes it, bulwark.adminConfigReadOnly, defaults to false because its price is threefold: it freezes the settings policy (and with it the only way to move Web Push off the vendor relay), it freezes an override that ALREADY exists instead of removing it, and on a fresh install it stops admin.json from being written at all - so the admin dashboard never comes up and its login answers HTTP 500 while the pod stays Ready. OIDC-only sign-in, with the real way back out of a lockout: not helm upgrade with OAUTH_ONLY=false (oauthOnly goes through the same precedence chain) but removing the override in the admin dashboard. That is why ADMIN_PASSWORD is set - the dashboard is the door. It generates no random password and logs nothing, so the #65 analogy does not apply. SESSION_SECRET_FILE and OAUTH_CLIENT_SECRET_FILE are both used - the better path garage (#81) and keycloak (#83) did not have - in one projected volume mounted WITHOUT subPath, because both are re-read on every use and a subPath mount is never refreshed, and at mode 0440 because secret files are owned by root and fsGroup only supplies the group. Two backup statements, both at the value: without the session secret the synchronized settings are unreadable out of an intact volume, and if the dashboard's OAuth setup was ever used the client secret sits in config.json on that volume in cleartext. Probes: all three a plain GET on /api/health, with the limit written at the readiness value - it carries process, HTTP acceptance, a free event loop and Next.js routing, and nothing about the reported state or about Stalwart. No ConfigMap, so the checksum/config rule (#82) has nothing to attach to, and values.yaml says so explicitly; the op:// paths are covered by checksum/secret-ref. Renovate: no versioningTemplate, measured rather than assumed. 315 tags in eight shapes (releases, a second v-prefixed family of the same images, an -always flavour, floating minors and names, commit SHAs, betas, attestations) and plain docker versioning excludes all six unwanted ones. Dry runs at 1.12.0, 1.11.0 and 1.10.0 are recorded in the block. Part of #89 - deliberately not "Fixes", because one binding decision of the issue was changed out of necessity and wants ratification rather than an automatic close: the plan specifies HEAD /api/health for all three probes, and a Kubernetes probe cannot send HEAD at all (HTTPGetAction has no method field; the kubelet always issues GET). The alternative, an exec probe, would mean starting a Node process every few seconds in an image that carries no curl and no wget. GET is used instead and is marginally more capable than HEAD, so every limit the issue argues is unchanged - but the decision is the maintainer's to confirm.
465 lines
20 KiB
Smarty
465 lines
20 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: nothing outside the Ingress template FUNCTIONALLY reads ingress.domain.
|
|
Bulwark derives its own public URL from the request origin and takes no base-URL
|
|
variable, so with ingress.enabled: false the release is complete without a
|
|
domain - the calibre-web-automated case, where the gate frees both values. The
|
|
enforcement therefore sits entirely in bulwark.ingress.yaml, where it is reached
|
|
by every render that has the Ingress on (the default), and this helper renders a
|
|
visible placeholder instead of failing.
|
|
*/}}
|
|
{{- define "bulwark.publicUrl" -}}
|
|
{{- if .Values.ingress.domain -}}
|
|
https://{{ .Values.ingress.domain }}
|
|
{{- else -}}
|
|
https://<ingress.domain NOT SET>
|
|
{{- end -}}
|
|
{{- end -}}
|