{{- $replicas := include "rallly.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.rallly.replicas) -}}
{{- $startup := fromYaml (include "rallly.startupProbe" .Values.rallly.startupProbe) -}}
{{- $limits := (fromYaml (include "rallly.effectiveResources" .Values.rallly.resources)).limits | default dict -}}
{{- $ephLimit := index $limits "ephemeral-storage" -}}
{{- $deployment := printf "%s-%s-deployment" .Release.Name .Chart.Name -}}
{{- $service := printf "%s-%s-service" .Release.Name .Chart.Name -}}
{{- $envNames := list -}}
{{- range $entry := .Values.rallly.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 -}}
Rallly {{ .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
{{- else if $httpRoute.enabled }}

ACCESS — GATEWAY MODE (httpRoute.enabled: true, ingress.enabled: false)
{{- range $hostnames }}
  https://{{ . }}
{{- end }}

  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.

  ingress.annotations is inert in this mode — the Gateway API has no annotation
  channel — and nothing this chart needs lives there.

  MIND THE HOSTNAME IN THIS MODE. NEXT_PUBLIC_BASE_URL still follows
  ingress.domain ("https://{{ .Values.ingress.domain }}"), not httpRoute.hostnames, and the OIDC
  redirect URI registered at the provider is built from it. Publishing under a
  name that ingress.domain does not carry gives you an instance whose links and
  sign-in all leave for the other host.
{{- 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. ingress.domain still is — see below.

    kubectl -n {{ .Release.Namespace }} port-forward svc/{{ $service }} 3000:80

  Then open http://127.0.0.1:3000/. That is a health check, not a working
  instance: the app still builds every link and the OIDC redirect from
  NEXT_PUBLIC_BASE_URL, which is "https://{{ .Values.ingress.domain }}" regardless of how
  you reached the port. A sign-in started over the forward leaves for that host.
{{- end }}

  ingress.domain is not only the Ingress host. The chart passes it to the app as
  NEXT_PUBLIC_BASE_URL — upstream's "fully qualified public URL of your
  instance" — which is why it stays required in every mode, with the Ingress
  switched off and in Gateway mode alike.

SIGN-IN GOES THROUGH THE PROVIDER, AND ONLY THROUGH THE PROVIDER
  This chart sets EMAIL_LOGIN_ENABLED and REGISTRATION_ENABLED to "false" in the
  template, not through a value: there is no magic-link login and no
  self-registration to fall back on. Every account arrives through the OIDC
  provider, shown on the login page as "{{ .Values.rallly.oidc.name }}".
  A wrong OIDC item therefore does not degrade the login, it removes it —
  including your own way back in.

  The redirect URI to register at the provider is

    https://{{ .Values.ingress.domain }}/api/auth/callback/oidc

  and the claim paths the app reads out of the token are
  name="{{ .Values.rallly.oidc.nameClaimPath }}", email="{{ .Values.rallly.oidc.emailClaimPath }}", picture="{{ .Values.rallly.oidc.pictureClaimPath }}".
  A provider that nests them somewhere else answers with a user who has no name
  or no address; that is these three values, not the provider.
{{ if eq (printf "%v" .Values.rallly.allowedEmails) "*" }}
  ALLOWED_EMAILS is "*", so every account the provider authenticates may sign in
  and the only gate in front of this instance is the provider's own audience.
  With a broad IdP that is an open door. Narrow it with a comma-separated list
  in rallly.allowedEmails; wildcards are supported (e.g. *@example.com).
{{- else }}
  ALLOWED_EMAILS is "{{ .Values.rallly.allowedEmails }}".
  An address outside that list is rejected after a successful login at the
  provider, so the symptom arrives one step away from its cause: the provider
  reports success and Rallly still refuses. Comma-separated, wildcards
  supported.
{{- end }}

  Admin rights are claimed by INITIAL_ADMIN_EMAIL from the secret below. It has
  to match the address the provider returns in the email claim exactly — nothing
  reconciles the two, and a mismatch simply leaves the instance without an admin.

SECRETS — THE FIELDS EACH 1PASSWORD ITEM MUST CARRY
  The CRs carry an item path only; the field names below are decided in this
  chart and appear nowhere in 1Password, so they are what you would otherwise
  read out of the templates. Every one of them is wired as a plain secretKeyRef
  without "optional: true": a field missing from the item does not fail this
  install — the operator writes a Secret without that key and the pod stays in
  CreateContainerConfigError. The key has to exist; an empty value is accepted,
  but the app then sees an empty string rather than its own default.

  {{ .Values.secrets.ralllySecretRef }}
    → Secret {{ .Release.Name }}-{{ .Chart.Name }}-secret
      secret-key             SECRET_PASSWORD. Upstream requires at least 32
                             characters. It encrypts the sessions, so changing
                             it signs everyone out.
      initial-admin-email    INITIAL_ADMIN_EMAIL, see above.

  {{ .Values.secrets.databaseSecretRef }}
    → Secret {{ .Release.Name }}-{{ .Chart.Name }}-database-secret
      database-url           postgres://USER:PASSWORD@HOST:5432/DATABASE

    The chart brings no database. Postgres has to exist and be reachable from
    this namespace before the release is of any use.

  {{ .Values.secrets.oidcSecretRef }}
    → Secret {{ .Release.Name }}-{{ .Chart.Name }}-oidc-secret
      discovery-url          the provider's .well-known/openid-configuration
      client-id
      client-secret

    The discovery URL is fetched by the pod, not by your browser: an IdP that
    only resolves outside the cluster, or one behind a policy this namespace
    cannot pass, fails here while looking perfectly reachable from your desk.
{{- if .Values.rallly.smtp.enabled }}

  {{ .Values.secrets.smtpSecretRef }}
    → Secret {{ .Release.Name }}-{{ .Chart.Name }}-smtp-secret
      host   port   secure   user   password
      no-reply-email   no-reply-email-name   tls-servername

    All eight are required keys here, including the three that are optional
    upstream (port, secure, tls-servername) and no-reply-email, which upstream
    would default to SUPPORT_EMAIL. An item carrying only host, user and
    password wedges the pod before it starts.
{{- if not .Values.rallly.smtp.rejectUnauthorized }}

    rallly.smtp.rejectUnauthorized is false: the mail server's TLS certificate
    is not verified and the SMTP session is open to a man-in-the-middle. That is
    the setting for a self-signed certificate only — prefer trusting the CA, and
    set it back to true once the server has a certificate that chains.
{{- end }}
{{- else }}

  MAIL IS OFF
    rallly.smtp.enabled is false, so no SMTP item is created and the container
    receives none of the SMTP_* variables — upstream lists SMTP_HOST among the
    required ones. Nothing the app tries to send leaves this pod: invitations
    and notification mails fail where they are sent, with a green rollout and a
    healthy pod either way. Sign-in is unaffected, because email login is off.
{{- end }}

ROTATING AN ITEM IN 1PASSWORD RESTARTS NOTHING
  Helm never sees the content of a OnePasswordItem, so no checksum annotation
  can cover these Secrets and this chart carries none. Change a field in
  1Password and the operator updates the Kubernetes Secret, while the running
  pod keeps what it read at start — environment variables are read once. Roll it
  yourself:

    kubectl -n {{ .Release.Namespace }} rollout restart deployment/{{ $deployment }}

  This bites hardest on database-url and the SMTP password: the app goes on
  using the old credentials, and nothing in the cluster says so.

WHAT A GREEN ROLLOUT DOES NOT PROVE
  All three probes are the same plain HTTP GET "/" against the container. They
  establish that the web process answers, and nothing beyond it: the OIDC
  provider, the mail server and the addresses in ALLOWED_EMAILS are exercised by
  the first real sign-in and the first real mail, never by a probe.
  The startup budget before the container is restarted is
  {{ $startup.periodSeconds }}s x {{ $startup.failureThreshold }} failures = {{ mul $startup.periodSeconds $startup.failureThreshold }}s, and every misconfiguration listed
  above outlives it untouched.

THE CACHE VOLUME COUNTS AGAINST THE EPHEMERAL-STORAGE LIMIT
  Next.js writes prerendered pages and the optimized images of /_next/image to
  {{ .Values.rallly.cache.path }}.
  /app belongs to the image's own user while the container runs as uid 20000
  under a read-only root filesystem, so the chart mounts a writable emptyDir
  over that path.
{{ if .Values.rallly.cache.sizeLimit }}
  The volume is capped at {{ .Values.rallly.cache.sizeLimit }}{{ if $ephLimit }}, and its usage counts a second time against
  the container's ephemeral-storage limit of {{ $ephLimit }}{{ end }}. Whichever bound is reached
  first, the kubelet evicts the pod — which reads as a restart with no
  application error anywhere in the log.
{{- else }}
  The volume has no size limit of its own{{ if $ephLimit }}, so it is bounded only by the
  container's ephemeral-storage limit of {{ $ephLimit }} — emptyDir usage counts against
  that limit, and exceeding it evicts the pod, which reads as a restart with no
  application error anywhere in the log{{ end }}. rallly.cache.sizeLimit sets an
  explicit bound.
{{- end }}
  The cache is volatile on purpose: after every restart it starts out empty and
  is rebuilt on demand, at the cost of a slower first request.

CHANGING THE DOMAIN LATER IS A CHANGE ON TWO SIDES
  ingress.domain is the published host, the app's NEXT_PUBLIC_BASE_URL and
  therefore the OIDC redirect URI in one value. Editing it here alone breaks the
  login: the provider refuses a callback it was never told about, and it is the
  provider that reports it — Kubernetes reports a healthy pod{{ if .Values.ingress.enabled }} and a valid
  certificate for the new name{{ else if $httpRoute.enabled }} and an accepted
  route{{ end }}. Register the new redirect URI first, then upgrade.
{{- if $httpRoute.enabled }}
  In GATEWAY MODE there is a third side: httpRoute.hostnames, which is what the
  route actually publishes. It does not follow ingress.domain once set
  explicitly, so a migration over a second hostname means the provider, the
  route and ingress.domain all have to end up agreeing.
{{- end }}
{{- if $envNames }}

rallly.env ADDS {{ len $envNames }} VARIABLE(S): {{ join ", " $envNames }}
  The block is additive — it is appended after the variables the chart sets
  itself, and it cannot remove any of them. A name the chart already sets ends
  up in the container spec twice; do not settle that by ordering, use the
  chart's own value for it instead ({{ .Chart.Name }}.smtp, {{ .Chart.Name }}.oidc, {{ .Chart.Name }}.allowedEmails).
{{- end }}
{{- if eq $replicas "0" }}

SCALED TO ZERO
  {{ if .Values.scaleDown }}scaleDown: true{{ else }}rallly.replicas: 0{{ end }} — the Deployment runs with replicas: 0, so nothing answers
  until you set it back and upgrade again. Secrets and Service{{ if .Values.ingress.enabled }}, and the Ingress
  with its certificate,{{ else if $httpRoute.enabled }}, and the HTTPRoute,{{ end }} stay in place.
{{- end }}
{{- if .Values.networkPolicy.enabled }}

NETWORKPOLICY IS ACTIVE — WHAT IT CAPS, AND WHAT IT DOES NOT
  {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy selects this workload's pods.
  Inbound: TCP 3000 only. Outbound: DNS, TCP 5432 (Postgres) and 443 (OIDC){{ if .Values.rallly.smtp.enabled }},
  plus 587 and 465 for mail submission (rallly.smtp.enabled is true){{ else }}.
  No SMTP rule is rendered, because rallly.smtp.enabled is false{{ end }}{{ with .Values.networkPolicy.egress }}{{ with .extraPorts }}, plus
  TCP {{ join ", " . }}{{ end }}{{ end }}.

  THOSE ARE THE STANDARD PORTS, NOT YOUR CONFIGURED ONES. A mail server on 25
  or 2525, or an OIDC issuer that is not on 443, is blocked until its port is in
  networkPolicy.egress.extraPorts — and a blocked mail server shows up as
  invitations that are never delivered, with a Ready pod behind it.

  Three limits belong in the same breath, because none of them is visible in
  the object itself:

  - EGRESS IS BY PORT, NEVER BY DESTINATION. A NetworkPolicy matches CIDRs and
    selectors and cannot express a hostname, so every egress rule permits its
    port to ANY address. That is a limit of the object, not a shortcut — this
    cluster's CNI (Canal: Flannel plus Calico) has no FQDN rule either. An
    outbound port the chart does not know about is BLOCKED: add it to
    networkPolicy.egress.extraPorts, and expect to find out through an
    application-level connection error rather than through this object.
{{- if .Values.networkPolicy.probeSources }}
  - THE PROBE PATH IS RESTRICTED in this release, because
    networkPolicy.probeSources is set to
      {{ join ", " .Values.networkPolicy.probeSources }}
    Probes come from the IP of the WORKER NODE running the pod. If none of those
    CIDRs covers the node this pod lands on, the kubelet's probes are dropped
    and THE POD NEVER BECOMES READY — the rollout then times out and the symptom
    looks like a broken application. Compare

      kubectl -n {{ .Release.Namespace }} describe pod -l app={{ .Release.Name }}-{{ .Chart.Name }}-deployment

    against `kubectl get node -o wide`.
{{- else }}
  - THE PROBED PORT IS OPEN INSIDE THE CLUSTER. networkPolicy.probeSources is
    empty, so that rule carries no source selector at all and any pod the CNI
    routes to can reach it. Not from the internet — but this is NOT pod-level
    isolation. Narrowing it means listing the worker node IPs in
    networkPolicy.probeSources, because the kubelet probes from the node's own
    address and the chart cannot know it; a wrong guess there leaves the pod
    permanently un-Ready.
{{- end }}
  - IT IS ONLY A BOUNDARY WHERE THE CNI ENFORCES POLICIES. Where it does not,
    this object is accepted and filters nothing, so everything reads as
    configured while nothing is filtered. `kubectl -n {{ .Release.Namespace }} get networkpolicy`
    shows the object exists; only a connection attempt from an unrelated
    namespace shows that it bites.

  Switch it off with networkPolicy.enabled: false.
{{- end }}
