Validate Helm Charts / Detect changed charts (pull_request) Successful in 5s
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / Build bentopdf (pull_request) Successful in 9s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 17s
Validate Helm Charts / Trivy config bentopdf (pull_request) Successful in 19s
Validate Helm Charts / Build garage (pull_request) Successful in 8s
Validate Helm Charts / Build patchmon (pull_request) Successful in 7s
Validate Helm Charts / Build rallly (pull_request) Successful in 7s
Validate Helm Charts / Build searxng (pull_request) Successful in 6s
Validate Helm Charts / Build sure (pull_request) Successful in 7s
Validate Helm Charts / Build urlaubsverwaltung (pull_request) Successful in 6s
Validate Helm Charts / Trivy config garage (pull_request) Successful in 18s
Validate Helm Charts / Build voucher-vault (pull_request) Successful in 7s
Validate Helm Charts / Trivy config patchmon (pull_request) Successful in 16s
Validate Helm Charts / Trivy config rallly (pull_request) Successful in 18s
Install-test / Install-test bentopdf (pull_request) Successful in 1m10s
Validate Helm Charts / Trivy config searxng (pull_request) Successful in 22s
Validate Helm Charts / Trivy config sure (pull_request) Successful in 18s
Validate Helm Charts / Trivy config urlaubsverwaltung (pull_request) Successful in 17s
Validate Helm Charts / Trivy config voucher-vault (pull_request) Successful in 19s
Install-test / Install-test garage (pull_request) Successful in 1m17s
Install-test / Install-test patchmon (pull_request) Successful in 1m24s
Install-test / Install-test rallly (pull_request) Successful in 1m39s
Install-test / Install-test searxng (pull_request) Successful in 1m7s
Install-test / Install-test sure (pull_request) Successful in 2m39s
Install-test / Install-test voucher-vault (pull_request) Successful in 2m9s
Validate Helm Charts / Install-test (pull_request) Successful in 13m15s
Install-test / Install-test urlaubsverwaltung (pull_request) Successful in 1m50s
Publish BentoPDF Helm Chart / Publish BentoPDF Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 9s
Publish Garage Helm Chart / Publish Garage Helm Chart (push) Successful in 9s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 9s
Publish Rallly Helm Chart / Publish Rallly Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish SearXNG Helm Chart / Publish SearXNG Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 9s
Publish Sure Helm Chart / Publish Sure Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish Urlaubsverwaltung Helm Chart / Publish Urlaubsverwaltung Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish Patchmon Helm Chart / Publish Patchmon Helm Chart (push) Successful in 9s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish VoucherVault Helm Chart / Publish VoucherVault Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
SonarQube Analysis / SonarQube Analysis (push) Successful in 43s
Group 1 of the #114 sweep: the charts that render exactly one Ingress object and set no behaviour-bearing nginx annotation - bentopdf, garage, patchmon, rallly, searxng, sure, urlaubsverwaltung, voucher-vault. Each gets the form the pilot (cyberchef, PR #115) fixed and template-chart carries: an httpRoute block (enabled/parentRefs/hostnames, off by default), a template rendering ONLY an HTTPRoute with parentRefs - no Gateway, no gatewayClassName, no Certificate, no implementation-specific policy object - render aborts on both modes at once (naming both value paths), on an empty parentRefs and on a missing hostname, empty hostnames meaning [ingress.domain], a schema entry closed against unknown keys with an open parentRef definition, and a NOTES.txt that names the active mode and what the mode costs. Minor, additive, defaults unchanged: `helm template` with each chart's CI values is byte-identical to origin/main for seven of the eight, and for sure identical in every rendered value (the diff is two YAML comment lines). Three findings that were not in the plan, each argued in the file it changes: sure derived RAILS_ASSUME_SSL/RAILS_FORCE_SSL from ingress.enabled alone. Gateway mode sets that false by construction while TLS is still terminated at the Gateway, so both flags would have gone false: insecure cookies, no HSTS, http:// links, with a Ready pod and a working UI as the only symptom. They now read sure.publishedOverTls (ingress.enabled OR httpRoute.enabled), which is ingress.enabled while httpRoute.enabled is false - hence still Minor. garage claimed, in its Ingress template and its schema, that nothing outside that template reads ingress.domain. The HTTPRoute reads it, so the claim now holds only while both modes are off. Corrected rather than left standing. garage also depends on an upload limit it does not set: its annotations comment tells the operator to set proxy-body-size: "0" because ingress-nginx truncates S3 uploads at 1 MB. proxy-body-size has no Gateway-API counterpart, so its Gateway-mode NOTES prints that the limit is the Gateway's. README's list of affected charts named five and now names garage as a sixth. clusterIssuer needed no change in any of the eight: its only required() sits inside the Ingress template behind ingress.enabled, the same no-op the pilot found. Annotation guidance that stops applying in Gateway mode moved into the Gateway branch of NOTES.txt for bentopdf (X-Robots-Tag) and searxng (forward-auth). CI: one render overlay per chart under ci/httproute/, so the assertion is not vacuum-green - ci/run-httproute-match-test.sh goes from 4 held assertions over 2 charts to 20 over 10. The install-test fixtures stay in Ingress mode: the install runs one configuration per chart, cyberchef already pays that price against the real Gateway-API CRDs, and for garage the Ingress is the only place its Service port is referenced by NAME. Not proven, by construction: that traffic flows. No Gateway controller runs in CI. Not run locally either: k3d, Docker, Trivy. Part of #114
419 lines
22 KiB
Plaintext
419 lines
22 KiB
Plaintext
{{- $replicas := include "urlaubsverwaltung.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.urlaubsverwaltung.replicas) -}}
|
|
{{- $res := fromYaml (include "urlaubsverwaltung.effectiveResources" .Values.urlaubsverwaltung.resources) -}}
|
|
{{/*
|
|
Both figures go through formatNumber for the reason issue #157 documents: they
|
|
come back from fromYaml as float64, and a bare byte count would print as
|
|
"1e+06" here just as it did in the rendered manifest.
|
|
*/}}
|
|
{{- $memLimitRaw := index ($res.limits | default dict) "memory" -}}
|
|
{{- $memLimit := "the computed memory limit" -}}
|
|
{{- if not (kindIs "invalid" $memLimitRaw) -}}
|
|
{{- $memLimit = include "urlaubsverwaltung.formatNumber" $memLimitRaw -}}
|
|
{{- end -}}
|
|
{{- $memRequestRaw := index ($res.requests | default dict) "memory" -}}
|
|
{{- $memRequest := "the memory request" -}}
|
|
{{- if not (kindIs "invalid" $memRequestRaw) -}}
|
|
{{- $memRequest = include "urlaubsverwaltung.formatNumber" $memRequestRaw -}}
|
|
{{- end -}}
|
|
{{/*
|
|
The effective limitFactor. It cannot be read back out of $res: that dict is the
|
|
RENDERED resources block (requests / limits only), so $res.limitFactor is always
|
|
empty and would silently print the default even where another factor is in
|
|
force. Mirror the defaulting of the resources helper on the raw value instead.
|
|
*/}}
|
|
{{- $resIn := .Values.urlaubsverwaltung.resources -}}
|
|
{{- if kindIs "invalid" $resIn -}}
|
|
{{- $resIn = dict -}}
|
|
{{- end -}}
|
|
{{- $factor := 3 -}}
|
|
{{- if not (kindIs "invalid" (index $resIn "limitFactor")) -}}
|
|
{{- $factor = index $resIn "limitFactor" -}}
|
|
{{- end -}}
|
|
{{/* An explicitly set memory limit wins over the computed one - say which. */}}
|
|
{{- $limitsIn := index $resIn "limits" -}}
|
|
{{- if kindIs "invalid" $limitsIn -}}
|
|
{{- $limitsIn = dict -}}
|
|
{{- end -}}
|
|
{{- $memLimitExplicit := not (kindIs "invalid" (index $limitsIn "memory")) -}}
|
|
{{- $startup := fromYaml (include "urlaubsverwaltung.startupProbe" .Values.urlaubsverwaltung.startupProbe) -}}
|
|
{{- $budget := "" -}}
|
|
{{- if and (kindIs "float64" $startup.periodSeconds) (kindIs "float64" $startup.failureThreshold) -}}
|
|
{{- $budget = printf "%.0f seconds" (mulf $startup.periodSeconds $startup.failureThreshold) -}}
|
|
{{- end -}}
|
|
{{- $envNames := list -}}
|
|
{{- range $entry := .Values.urlaubsverwaltung.env }}
|
|
{{- $envNames = append $envNames $entry.name }}
|
|
{{- end }}
|
|
{{- $httpRoute := .Values.httpRoute | default dict -}}
|
|
{{- $hostnames := $httpRoute.hostnames | default list -}}
|
|
{{- if not $hostnames -}}
|
|
{{- $hostnames = list .Values.ingress.domain -}}
|
|
{{- end -}}
|
|
Urlaubsverwaltung {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
|
|
|
{{- if .Values.ingress.enabled }}
|
|
|
|
ACCESS — INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
|
|
https://{{ .Values.ingress.domain }}
|
|
|
|
The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}"
|
|
cluster issuer. Until it has been issued the browser shows a warning; follow it with
|
|
|
|
kubectl -n {{ .Release.Namespace }} get certificate tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
|
|
|
The Ingress publishes the application at "/" with no path restriction, so
|
|
everything the app serves is served under this host — including
|
|
/swagger-ui.html and /v3/api-docs, which this app version enables by default
|
|
and this chart does not turn off. Both are behind the login (they answer 302
|
|
to the identity provider like every other path), so this is exposure of an
|
|
API description, not of data. Turning them off is a
|
|
springdoc.api-docs.enabled / springdoc.swagger-ui.enabled property; the chart
|
|
sets neither, so it can be added through urlaubsverwaltung.env.
|
|
{{- else if $httpRoute.enabled }}
|
|
|
|
ACCESS — GATEWAY MODE (httpRoute.enabled: true, ingress.enabled: false)
|
|
{{- range $hostnames }}
|
|
https://{{ . }}
|
|
{{- end }}
|
|
|
|
The route publishes the application at "/" with no path restriction, exactly
|
|
as the Ingress does — including /swagger-ui.html and /v3/api-docs, which this
|
|
app version enables by default and this chart does not turn off. Both are
|
|
behind the login, so this is exposure of an API description, not of data;
|
|
springdoc.api-docs.enabled / springdoc.swagger-ui.enabled through
|
|
urlaubsverwaltung.env is how to turn them off.
|
|
|
|
This release renders an HTTPRoute ({{ .Release.Name }}-{{ .Chart.Name }}-httproute) and NOTHING else
|
|
of the routing path — no Gateway, no class, no Certificate. Three things
|
|
therefore belong to the cluster and not to this chart:
|
|
|
|
1. The Gateway-API CRDs. Without them this install would have failed.
|
|
2. The Gateway named in httpRoute.parentRefs
|
|
{{- range $httpRoute.parentRefs }}
|
|
{{ .name }}{{ if .namespace }} in namespace {{ .namespace }}{{ end }}{{ if .sectionName }}, listener {{ .sectionName }}{{ end }}
|
|
{{- end }}
|
|
must exist AND its listener must allow routes from this namespace
|
|
({{ .Release.Namespace }}) in allowedRoutes. A rejected attachment shows
|
|
up only in the route's status:
|
|
|
|
kubectl -n {{ .Release.Namespace }} get httproute {{ .Release.Name }}-{{ .Chart.Name }}-httproute -o yaml
|
|
|
|
Read status.parents[].conditions — "Accepted: True" and
|
|
"ResolvedRefs: True" is a working attachment. An empty status means no
|
|
controller has looked at the route at all.
|
|
3. The TLS certificate for the hostname, on the Gateway's listener.
|
|
ingress.clusterIssuer is NOT used in this mode and is not required: the
|
|
chart renders no Certificate, so there is no
|
|
tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress secret and nothing for cert-manager to do.
|
|
|
|
THE HOSTNAME AND THE APPLICATION'S IDEA OF ITSELF STILL DO NOT AGREE BY
|
|
THEMSELVES. No environment variable of the container derives from
|
|
ingress.domain or from httpRoute.hostnames; the address the application
|
|
considers its own is the OIDC redirect URI, out of the 1Password item below.
|
|
The two can drift apart without any complaint — the route answers, the login
|
|
bounces.
|
|
|
|
ingress.annotations is inert in this mode: the Gateway API has no annotation
|
|
channel. Nothing this chart needs lives there.
|
|
{{- else }}
|
|
|
|
ACCESS — NEITHER INGRESS NOR HTTPROUTE
|
|
ingress.enabled and httpRoute.enabled are both false, so this release has no
|
|
Ingress, no HTTPRoute and no certificate;
|
|
ingress.clusterIssuer is not needed, and neither is ingress.domain. Unlike
|
|
other charts in this repo, ingress.domain is read by NOTHING here except the
|
|
two routing templates — no environment variable of the container derives from
|
|
it. The address the application considers its own is the OIDC redirect URI,
|
|
and that comes out of the 1Password item below, not out of this value. The
|
|
two can drift apart without any complaint.
|
|
|
|
Reach the release inside the cluster with
|
|
|
|
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80
|
|
|
|
and open http://localhost:8080. The login redirects to your identity
|
|
provider, which will refuse to come back to a localhost callback unless that
|
|
exact URL is registered there as well.
|
|
{{- end }}
|
|
{{ if eq $replicas "0" }}
|
|
SCALED TO ZERO
|
|
{{ if .Values.scaleDown }}scaleDown: true{{ else }}urlaubsverwaltung.replicas: 0{{ end }} — the Deployment runs with replicas: 0, so the endpoint
|
|
above does not answer until you set it back and upgrade again. Both
|
|
OnePasswordItem CRs and the Service{{ if .Values.ingress.enabled }} and Ingress stay in place, so the
|
|
certificate is not re-issued.{{ else if $httpRoute.enabled }} and HTTPRoute stay in place, so the route
|
|
keeps pointing at a Service with no endpoints — the Gateway answers with an
|
|
error of its own rather than with nothing.{{ else }} stay in place.{{ end }} This chart owns no volume: all
|
|
state lives in the external database, so scaling to zero loses nothing.
|
|
{{ end }}
|
|
SECRETS — THE FIELDS EACH 1PASSWORD ITEM MUST CARRY
|
|
The operator copies an item's FIELDS into the Kubernetes Secret, one key per
|
|
field name, and this chart reads each key by name into an environment
|
|
variable. The field names are decided here and appear nowhere in 1Password,
|
|
so they are exactly what you would otherwise have to read out of the
|
|
deployment template. They must match to the letter.
|
|
|
|
secrets.databaseSecretRef → {{ .Values.secrets.databaseSecretRef }}
|
|
→ Secret {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
|
jdbc-url a full JDBC URL, e.g.
|
|
jdbc:postgresql://postgres.example.com:5432/urlaubsverwaltung
|
|
username
|
|
password
|
|
|
|
secrets.oidcSecretRef → {{ .Values.secrets.oidcSecretRef }}
|
|
→ Secret {{ .Release.Name }}-{{ .Chart.Name }}-oidc-secret
|
|
client-id
|
|
client-secret
|
|
issuer-name the provider's display name on the login button
|
|
issuer-uri the OIDC issuer, WITHOUT /.well-known/openid-configuration
|
|
jwt-issuer-uri the same issuer for the resource-server side
|
|
redirect-uri the full callback URL. This chart registers the client
|
|
under the id "default", so the path is
|
|
/login/oauth2/code/default — for example
|
|
https://{{ if or .Values.ingress.enabled $httpRoute.enabled }}{{ first $hostnames }}{{ else }}example.com{{ end }}/login/oauth2/code/default
|
|
and the same URL has to be registered at the provider.
|
|
|
|
This chart deploys NO database and NO identity provider. Both have to exist
|
|
already and be reachable from this namespace.
|
|
|
|
A field spelled differently yields a Secret without that key. The pod then
|
|
hangs in CreateContainerConfigError while the Secret itself exists, so it
|
|
looks as if the operator had done its job. What it actually delivered:
|
|
|
|
kubectl -n {{ .Release.Namespace }} get secret {{ .Release.Name }}-{{ .Chart.Name }}-db-secret -o jsonpath='{.data}'
|
|
|
|
Pointing a ref at a DIFFERENT item rolls the pod; changing a value INSIDE the
|
|
same item does not. Helm never sees that content, so the rendered Deployment
|
|
is unchanged and this chart stamps no checksum over the secrets — nothing
|
|
restarts, and the pod keeps running with the old values:
|
|
|
|
kubectl -n {{ .Release.Namespace }} rollout restart deployment/{{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
|
|
|
THE IDENTITY PROVIDER IS REQUIRED AT BOOT, NOT AT LOGIN
|
|
On start the application resolves the provider's configuration itself, with a
|
|
GET on
|
|
|
|
<issuer-uri>/.well-known/openid-configuration
|
|
|
|
and it does this while building its security beans — before the HTTP port
|
|
opens. An issuer-uri that does not resolve, is unreachable from this
|
|
namespace, or answers something other than a discovery document therefore
|
|
does not produce a broken login page: the process exits with
|
|
|
|
Application run failed ... Unable to resolve Configuration with the
|
|
provided Issuer of "<issuer-uri>"
|
|
|
|
and the pod goes into CrashLoopBackOff having never served a request. So a
|
|
provider that is down at rollout time keeps this release from starting at
|
|
all. The reason is only in the log:
|
|
|
|
kubectl -n {{ .Release.Namespace }} logs deployment/{{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
|
|
|
Note the order: the database migration runs BEFORE this step. A rollout that
|
|
dies on the identity provider has already migrated the schema.
|
|
|
|
STARTUP MIGRATES THE DATABASE, AND ROLLBACK DOES NOT UNMIGRATE
|
|
Liquibase brings the schema up to date on every start, before anything is
|
|
served; against an empty database it creates it from scratch and the app then
|
|
seeds its initial settings, vacation types and sick-note types. The startup
|
|
probe covers that whole window{{ if $budget }} ({{ $budget }}){{ end }} and holds liveness and readiness off
|
|
until it is through. A measured cold start against an empty database — JVM,
|
|
full migration, seeding — took about 30 seconds; an upgrade that carries a
|
|
long migration takes longer, and if it outruns the budget the kubelet kills
|
|
the container mid-migration and the next attempt starts over.
|
|
|
|
helm rollback puts the previous app version back; the schema does not come
|
|
back with it, because it was migrated in place. Take a database backup BEFORE
|
|
an app-version upgrade. There is no volume in this release to back up — the
|
|
database is the whole of the state.
|
|
|
|
A failed rollout does not take the release down: the previous pod keeps
|
|
serving until the new one becomes Ready. "helm upgrade" without --wait
|
|
therefore reports success while the new pod crashloops behind it. Check:
|
|
|
|
kubectl -n {{ .Release.Namespace }} rollout status deployment/{{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
|
|
|
THE MEMORY LIMIT SIZES THE JAVA HEAP — IT IS NOT JUST A CEILING
|
|
The image is built with Cloud Native Buildpacks, and its memory calculator
|
|
reads the container's memory LIMIT at every start and derives -Xmx and the
|
|
metaspace from it. In this release that limit is {{ $memLimit }}.
|
|
{{- if $memLimitExplicit }}
|
|
It is set explicitly under urlaubsverwaltung.resources.limits.memory, which
|
|
wins over the factor the chart would otherwise apply.
|
|
{{- else }}
|
|
The chart does not set it directly: it is computed as
|
|
urlaubsverwaltung.resources.limitFactor ({{ include "urlaubsverwaltung.formatNumber" $factor }})
|
|
x the memory request ({{ $memRequest }}).
|
|
{{- end }}
|
|
|
|
So the memory limit is not an operational safety margin here, it is a JVM
|
|
tuning knob. Lowering it shrinks the heap; raising it raises the heap.
|
|
A limit chosen too tightly does not merely risk an OOMKill under load —
|
|
the calculator refuses to start at all when what is left after metaspace,
|
|
code cache and the thread stacks is negative, and that failure appears before
|
|
any application log line. The sizing it chose is the first line of the log:
|
|
|
|
kubectl -n {{ .Release.Namespace }} logs deployment/{{ .Release.Name }}-{{ .Chart.Name }}-deployment | head -1
|
|
|
|
THE THREE HEALTH PATHS AND WHAT EACH ONE ANSWERS FOR
|
|
This chart sets two health settings in the deployment template, and they are
|
|
what makes the three paths mean different things:
|
|
|
|
MANAGEMENT_ENDPOINT_HEALTH_GROUP_READINESS_INCLUDE=readinessState,db
|
|
MANAGEMENT_HEALTH_MAIL_ENABLED=false
|
|
|
|
/actuator/health everything the release actually runs: db,
|
|
diskSpace, ping, ssl and the two lifecycle
|
|
states. 200 UP on a healthy release. The mail
|
|
indicator is NOT in it — see the mail section.
|
|
→ point external monitoring here. This is the only path that also fails on
|
|
diskSpace or ssl.
|
|
|
|
/actuator/health/readiness readinessState AND db. Goes DOWN (503) on a
|
|
database outage, which takes the pod out of Ready
|
|
and stops traffic reaching it.
|
|
→ the readinessProbe.
|
|
|
|
/actuator/health/liveness livenessState alone, deliberately WITHOUT db.
|
|
Stays 200 through a database outage.
|
|
→ the livenessProbe and the startupProbe.
|
|
|
|
The split is the point: a database outage has to keep traffic away, but
|
|
restarting the pod does not bring the database back. Adding db to the
|
|
liveness group would turn every outage into a crash loop on top of the
|
|
outage. Measured on this chart, with the database scaled to zero underneath a
|
|
running pod: readiness 503, liveness 200, pod 0/1 Ready, restart count 0 —
|
|
the pod left Ready about 20 seconds after the database went away.
|
|
|
|
The other half of that measurement matters just as much: when the database
|
|
came back, the pod returned to Ready by itself within about five seconds,
|
|
still with restart count 0. Nothing had to be rolled, deleted or scaled by
|
|
hand. That is what leaving the liveness group alone buys — the outage is
|
|
ridden out and then simply over.
|
|
|
|
How readiness actually fails is worth knowing before you tune it. The db
|
|
indicator does not answer 503 quickly — it waits out the JDBC connection
|
|
timeout first, measured at 30 seconds against a database whose Service has no
|
|
endpoints at all. readinessProbe.timeoutSeconds is 5, so the kubelet gives up
|
|
long before the answer arrives and logs
|
|
|
|
Readiness probe failed: ... context deadline exceeded
|
|
|
|
rather than a 503. The pod leaves Ready either way, and faster; the 503 is
|
|
what a client with patience sees. Raising timeoutSeconds past the JDBC
|
|
timeout would swap the timeout for the 503 and delay the reaction — there is
|
|
no reason to.
|
|
|
|
WHAT A READY POD DOES AND DOES NOT PROVE
|
|
Ready now means the pod can reach the database — that is checked on every
|
|
readiness period, not once at boot. It still does not mean the identity
|
|
provider is reachable: OIDC is resolved once at startup (see below) and no
|
|
health indicator watches it afterwards, so a provider that goes down after
|
|
the rollout leaves a Ready pod on which nobody can log in.
|
|
|
|
Ready also proves reachability, not correctness: the db indicator validates a
|
|
connection, it never looks at the schema. Measured here — a database that came
|
|
back EMPTY put the pod straight back to Ready with /actuator/health at 200,
|
|
while not one table existed in it. A restored-but-wrong database reads as
|
|
healthy, so a probe going green after a database incident is not on its own
|
|
evidence that the data came back with it.
|
|
|
|
A database outage now shows up in the pod's status: READY drops to 0/1 while
|
|
STATUS stays Running and RESTARTS stays put. That combination — running, not
|
|
restarting, not Ready — is the signature of a dependency outage rather than
|
|
of a broken release. The reason is in the endpoint, not in the log:
|
|
|
|
kubectl -n {{ .Release.Namespace }} get pod -l app={{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
|
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80
|
|
curl -s localhost:8080/actuator/health
|
|
|
|
The body names components only when show-details is on; the chart does not
|
|
set it, so the aggregate answers {"status":"DOWN","groups":[...]} and the
|
|
status code carries the message. To see WHICH component is down, add
|
|
|
|
urlaubsverwaltung.env:
|
|
- name: MANAGEMENT_ENDPOINT_HEALTH_SHOW_DETAILS
|
|
value: always
|
|
|
|
and know what that publishes to anyone who can reach the endpoint: the
|
|
database product, the container's filesystem path and free bytes, the
|
|
certificate chains, and the stack-trace class of whatever failed.
|
|
|
|
Note also that there is no shell in this image: "kubectl exec" cannot open
|
|
one, and neither can a debug sidecar sharing the process namespace run the
|
|
app's own tooling. Everything you can learn from a running pod, you learn
|
|
from its log and its HTTP endpoints.
|
|
|
|
MAIL AND THE APPLICATION URL ARE PLACEHOLDERS UNTIL YOU SET THEM
|
|
The chart hardcodes the mail wiring to values that work nowhere:
|
|
|
|
SPRING_MAIL_HOST=localhost, SPRING_MAIL_PORT=1025
|
|
UV_MAIL_FROM / UV_MAIL_REPLYTO = urlaubsverwaltung@example.org
|
|
UV_CALENDAR_ORGANIZER = organizer@example.org
|
|
UV_MAIL_APPLICATIONURL = http://localhost:8080
|
|
|
|
Nothing in this pod listens on localhost:1025, so no notification mail leaves
|
|
the release. UV_MAIL_APPLICATIONURL is what the app puts into the links
|
|
inside those mails — it is NOT derived from ingress.domain. Left as it is,
|
|
every link points at the reader's own machine.
|
|
|
|
BECAUSE that wiring is a placeholder, the chart sets
|
|
|
|
MANAGEMENT_HEALTH_MAIL_ENABLED=false
|
|
|
|
and the mail indicator is gone from /actuator/health. Otherwise the aggregate
|
|
would answer 503 DOWN on a perfectly healthy release for its whole lifetime,
|
|
and any monitor pointed at it would page forever about a mail server nobody
|
|
configured.
|
|
|
|
THE PART THAT BITES LATER: the day you configure real mail delivery, switch
|
|
the indicator back on in the SAME change —
|
|
|
|
urlaubsverwaltung.env:
|
|
- name: MANAGEMENT_HEALTH_MAIL_ENABLED
|
|
value: "true"
|
|
|
|
Left at false it does not report a false alarm, it reports nothing: the
|
|
endpoint answers 200 while the mail server is unreachable and notifications
|
|
are silently not delivered. The setting is only honest while there is no mail
|
|
server to be honest about.
|
|
|
|
Note that this variable repeats a name the chart already sets, so per the rule
|
|
below it only takes effect when it goes in at INSTALL time. Put into an
|
|
already installed release it does nothing at all, and says so nowhere: helm
|
|
reports a successful upgrade while the container keeps "false".
|
|
|
|
urlaubsverwaltung.env IS ADDITIVE ONLY, AND A COLLISION IS LOST ON UPGRADE
|
|
The entries are appended AFTER the chart's own list{{ if $envNames }} (this release appends:
|
|
{{ join ", " $envNames }}){{ end }}. An entry that repeats a name the
|
|
chart already sets does not replace it — it renders the variable twice, and
|
|
what happens to that pair depends on WHEN it first appears:
|
|
|
|
AT INSTALL both entries reach the pod and the later one — yours — wins in the
|
|
container. The API server accepts it with a warning:
|
|
|
|
Warning: spec.template.spec.containers[0].env[N]: hides previous definition
|
|
of "NAME", which may be dropped when using apply
|
|
|
|
AT UPGRADE that warning comes true. Kubernetes merges env by name, so the pair
|
|
collapses back to ONE entry and the surviving value is the CHART's, not yours.
|
|
Nothing fails: helm prints "Release has been upgraded", the revision counts up,
|
|
and the override is simply gone. Measured on this chart (helm 3.19.0,
|
|
Kubernetes 1.35): "helm get manifest" shows the variable twice while the live
|
|
Deployment shows it once, with the chart's value.
|
|
|
|
So the trap is silence, not an error. Treat the variables above as fixed for
|
|
the life of a release: to really change one, reinstall. And never read a green
|
|
upgrade as proof that an override took effect — read it off the object:
|
|
|
|
kubectl -n {{ .Release.Namespace }} get deployment {{ .Release.Name }}-{{ .Chart.Name }}-deployment \
|
|
-o jsonpath='{.spec.template.spec.containers[0].env}'
|
|
|
|
Anything sensitive belongs in valueFrom.secretKeyRef rather than a plain
|
|
value: a value is stored in the pod spec and readable by anyone who can read
|
|
pods in {{ .Release.Namespace }}.
|
|
{{- if not $envNames }}
|
|
|
|
urlaubsverwaltung.env is empty in this release, so the placeholders above are
|
|
live exactly as printed.
|
|
{{- end }}
|