initial commit
Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled

This commit is contained in:
Daniel Noulet 2026-10-07 15:11:35 +02:00
commit 951dd8d26b
175 changed files with 50041 additions and 0 deletions

View file

@ -0,0 +1,19 @@
# Files excluded from the packaged chart. Anything matched here is neither sent to
# the cluster nor included in `helm package`, which keeps the artifact small and
# stops a local editor's swap file from ever reaching a release.
.DS_Store
.git/
.gitignore
.idea/
.vscode/
*.swp
*.bak
*.tmp
*.orig
*.tgz
*.md
# Documentation is excluded from the packaged chart: it is several hundred
# kilobytes of prose that the cluster does not need, and a chart artifact
# should contain only what is required to deploy.
*.md

View file

@ -0,0 +1,36 @@
apiVersion: v2
name: go-bananas
description: >-
Runtime design-token service. Author design-token themes in a web interface and
serve them to any application as CSS custom properties at run time, with no npm
dependency and no build step on the consumer side.
type: application
# The chart version. Bumped for every change to the chart itself.
version: 1.0.0
# The version of the application this chart deploys, which follows the image tag.
# Keeping the two in step is what `helm upgrade --reuse-values` needs to roll
# forward correctly.
appVersion: "1.0.0"
kubeVersion: ">=1.28.0-0"
keywords:
- design-system
- design-tokens
- theming
- css-variables
- frontend
icon: https://raw.githubusercontent.com/urpi/go-bananas/main/docs/assets/logo.svg
home: https://github.com/urpi/go-bananas
sources:
- https://github.com/urpi/go-bananas
maintainers:
- name: urpi platform team
url: https://github.com/urpi
annotations:
category: DeveloperTools
licenses: MIT

View file

@ -0,0 +1,89 @@
{{- $name := include "go-bananas.fullname" . }}
go-bananas {{ .Chart.AppVersion }} is deployed as {{ $name }} in namespace {{ .Release.Namespace }}.
────────────────────────────────────────────────────────────────────────────
{{- if .Values.ingress.enabled }}
Your instance is reachable at:
{{- range .Values.ingress.hosts }}
{{- $scheme := "http" }}
{{- if $.Values.ingress.tls }}{{ $scheme = "https" }}{{ end }}
{{- range $.Values.ingress.hosts }}
{{ $scheme }}://{{ .host }}
{{- end }}
{{- break }}
{{- end }}
Follow the admin interface from there. Sign in with your identity provider.
{{- else if eq .Values.service.type "NodePort" }}
Your instance is on node port {{ .Values.service.nodePort }}. Find the address with:
kubectl get nodes -o wide
kubectl port-forward --address 0.0.0.0 svc/{{ $name }} 8080:{{ .Values.service.port }}
{{- else }}
Port-forward to reach the admin interface:
kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ $name }} 8080:{{ .Values.service.port }}
Then open http://localhost:8080
{{- end }}
────────────────────────────────────────────────────────────────────────────
Runtime integration
{{- if eq .Values.identity.mode "oidc" }}
Consumers fetch themes with no dependency and no build step. Add this to any
application's <head>:
<link rel="stylesheet"
href="{{ include "go-bananas.publicUrl" . }}/rt/v1/theme.css?slug=midnight">
Full instructions are at {{ include "go-bananas.publicUrl" . }}/rt/v1/embed?slug=midnight
Available themes:
{{- $base := printf "%s/rt/v1" (include "go-bananas.publicUrl" .) }}
{{- $base }}{{ "/theme.css?slug=<slug>" }}
{{- $base }}{{ "/theme.json?slug=<slug>" }}
{{- $base }}{{ "/manifest.json" }}
{{- $base }}{{ "/runtime.js" }} (framework-free ES module)
{{- $base }}{{ "/react.js" }} (React provider and hooks)
{{- $base }}{{ "/embed.js" }} (one-line script tag)
{{- else }}
Authentication is set to "{{ .Values.identity.mode }}". Every request is currently
treated as an administrator. Before exposing this to a network, set:
identity.mode=oidc
identity.oidc.issuer=https://auth-dev.urpi.be/realms/<realm>
identity.oidc.clientId=go-bananas
identity.oidc.existingSecret=<secret-name>
{{- end }}
────────────────────────────────────────────────────────────────────────────
Verify it is working
kubectl --namespace {{ .Release.Namespace }} get pods -l app.kubernetes.io/instance={{ .Release.Name }}
kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ $name }} 8080:{{ .Values.service.port }}
curl -s localhost:{{ .Values.service.port }}/readyz
{{- if .Values.config.enableMetrics }}
curl -s localhost:{{ .Values.service.port }}/metrics | head
{{- end }}
────────────────────────────────────────────────────────────────────────────
Notes
* The database is a single SQLite file on a {{ .Values.persistence.type | default "disabled" }} volume
{{- if and .Values.persistence.enabled (eq .Values.persistence.type "persistentVolumeClaim") }}
at {{ .Values.persistence.mountPath }}. It is retained on uninstall.
{{- end }}
* {{ .Values.deployment.replicaCount }} replica(s). With a ReadWriteOnce volume, more than one
replica needs a filesystem that permits concurrent mounts.
* Built-in themes are reconciled at start-up. A built-in theme an operator has
edited is never overwritten.

View file

@ -0,0 +1,256 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "go-bananas.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
A fully qualified app name, truncated to the 63-character DNS label limit.
The release name is included because a chart may legitimately be installed more
than once in a namespace: one for staging themes and one for production. Without
it, the two would collide on every object name.
*/}}
{{- define "go-bananas.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Chart name and version, as used by the standard chart label.
*/}}
{{- define "go-bananas.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Selector labels.
These are deliberately stable: a Deployment's selector is immutable, so changing
these labels on an existing release makes the upgrade fail. That failure is the
point — it prevents the controller from orphaning pods.
*/}}
{{- define "go-bananas.selectorLabels" -}}
app.kubernetes.io/name: {{ include "go-bananas.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Common labels, including the recommended Kubernetes labels and the operator's own.
*/}}
{{- define "go-bananas.labels" -}}
helm.sh/chart: {{ include "go-bananas.chart" . }}
{{ include "go-bananas.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: go-bananas
{{- with .Values.global.extraLabels }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{/*
The image reference, honouring the digest override.
A digest is preferred over a tag when both are available: it is the only way to
guarantee that a rollout is running exactly the image that was reviewed, rather
than whatever a mutable tag points at today.
Both branches build the reference the same way — registry, then repository path,
then the tag or digest — so the digest branch cannot drift into producing a
doubled registry prefix such as ghcr.io/ghcr.io/urpi/go-bananas.
*/}}
{{- define "go-bananas.image" -}}
{{- $registry := .Values.image.registry -}}
{{- if .Values.global.imageRegistry -}}
{{- $registry = .Values.global.imageRegistry -}}
{{- end -}}
{{- if not $registry -}}
{{- fail "image.registry must not be empty. It is the registry host, for example ghcr.io or registry.example.com." -}}
{{- end -}}
{{- $repository := .Values.image.repository -}}
{{- /* Convention: image.registry is the host and image.repository is the path.
But an operator who writes the host into repository should get the reference
they asked for, not ghcr.io/quay.io/other/app. A first path segment
containing a dot or a colon, or equal to localhost, is a registry host
rather than a namespace. */ -}}
{{- $first := first (splitList "/" $repository) -}}
{{- $looksQualified := or (eq $repository $registry) (hasPrefix (printf "%s/" $registry) $repository) (contains "." $first) (contains ":" $first) (eq $first "localhost") -}}
{{- $name := $repository -}}
{{- if not $looksQualified -}}
{{- $name = printf "%s/%s" $registry $repository -}}
{{- end -}}
{{- if .Values.image.digest -}}
{{- printf "%s@%s" $name .Values.image.digest -}}
{{- else -}}
{{- printf "%s:%s" $name (default .Chart.AppVersion .Values.image.tag) -}}
{{- end -}}
{{- end }}
{{/*
The ServiceAccount name to use.
*/}}
{{- define "go-bananas.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "go-bananas.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
Whether a generated Secret should be produced.
Either mode needs a Secret, but the two differ in ownership. An existing Secret is
the operator's, so the chart only reads it. A generated one is created when
generateSecret is true, and the value is preserved across upgrades so that a
rolling update does not silently invalidate every session.
*/}}
{{- define "go-bananas.usesExistingSecret" -}}
{{- if and .Values.identity.oidc.existingSecret .Values.identity.oidc.generateSecret -}}
{{- fail "identity.oidc.existingSecret and identity.oidc.generateSecret are mutually exclusive. Set generateSecret to false when supplying an existing Secret." -}}
{{- end -}}
{{- if not (or .Values.identity.oidc.existingSecret .Values.identity.oidc.generateSecret) -}}
{{- fail "identity.oidc needs a Secret. Set identity.oidc.existingSecret to a Secret you manage, or identity.oidc.generateSecret to true for a chart-managed one." -}}
{{- end -}}
{{- .Values.identity.oidc.existingSecret -}}
{{- end }}
{{/*
The name of the Secret holding the session and client secrets.
*/}}
{{- define "go-bananas.secretName" -}}
{{- if .Values.identity.oidc.existingSecret -}}
{{- .Values.identity.oidc.existingSecret -}}
{{- else -}}
{{- printf "%s-auth" (include "go-bananas.fullname" .) -}}
{{- end -}}
{{- end }}
{{/*
The key inside the Secret holding the session signing secret.
*/}}
{{- define "go-bananas.sessionSecretKey" -}}
{{- .Values.identity.sessionSecretKey -}}
{{- end }}
{{/*
The key inside the Secret holding the OIDC client secret.
*/}}
{{- define "go-bananas.clientSecretKey" -}}
{{- default .Values.identity.oidc.clientSecretKey .Values.identity.oidc.existingSecretKeys.clientSecret -}}
{{- end }}
{{/*
Validate the identity configuration before anything is rendered.
Failing at template time is much better than failing at run time: a misconfigured
release never reaches the API server, so there is no half-created Deployment and no
CrashLoopBackOff to diagnose. Each check names the value to change.
*/}}
{{- define "go-bananas.validate" -}}
{{- $mode := .Values.identity.mode -}}
{{- if not (has $mode (list "oidc" "dev" "disabled")) -}}
{{- fail (printf "identity.mode %q is not valid. Use oidc, dev or disabled." $mode) -}}
{{- end -}}
{{- if eq $mode "oidc" -}}
{{- if not .Values.identity.oidc.issuer -}}
{{- fail "identity.oidc.issuer is required when identity.mode is oidc. For the development realm this is https://auth-dev.urpi.be/realms/<realm>." -}}
{{- end -}}
{{- if not (regexMatch "^https?://" .Values.identity.oidc.issuer) -}}
{{- fail (printf "identity.oidc.issuer %q is not a URL. It must include the scheme." .Values.identity.oidc.issuer) -}}
{{- end -}}
{{- if not .Values.identity.oidc.clientId -}}
{{- fail "identity.oidc.clientId is required when identity.mode is oidc." -}}
{{- end -}}
{{- /* The result is discarded on purpose. This call exists for its fail-fast side
effect, and returning the Secret *name* from the helper would write it into
the rendered manifest as a bare line above `apiVersion`, which is invalid
YAML. It only went unnoticed because with the default generateSecret the
helper returns the empty string. */ -}}
{{- $_ := include "go-bananas.usesExistingSecret" . -}}
{{- end -}}
{{- if eq $mode "oidc" -}}
{{- /* Refused in production because it disables certificate verification on the
connection to the identity provider, which is the one connection where a
man-in-the-middle would be most damaging. */}}
{{- if and .Values.identity.oidc.skipTLSVerify (eq .Values.config.environment "production") -}}
{{- fail "identity.oidc.skipTLSVerify cannot be combined with config.environment: production. It disables certificate verification against the identity provider. Set config.environment to development, or supply a trusted CA instead." -}}
{{- end -}}
{{- end -}}
{{- if not .Values.config.publicBaseUrl -}}
{{- fail "config.publicBaseUrl is required. It is used to build the OIDC redirect URI, the absolute URLs in the runtime manifest, and the HSTS decision." -}}
{{- end -}}
{{- if not (regexMatch "^https?://[^/]+" .Values.config.publicBaseUrl) -}}
{{- fail (printf "config.publicBaseUrl %q must be an absolute URL with a scheme and host, for example https://themes.example.com" .Values.config.publicBaseUrl) -}}
{{- end -}}
{{- if hasSuffix "/" .Values.config.publicBaseUrl -}}
{{- fail (printf "config.publicBaseUrl %q must not end with a slash. Use https://themes.example.com, not https://themes.example.com/." .Values.config.publicBaseUrl) -}}
{{- end -}}
{{- if and (gt (int .Values.config.shutdownTimeoutMs) 0) (gt (int .Values.deployment.terminationGracePeriodSeconds) 0) -}}
{{- /* The grace period is in seconds and the timeout in milliseconds, so the
comparison converts one to the other rather than comparing 25000 to 45. */}}
{{- $graceMs := mul (int .Values.deployment.terminationGracePeriodSeconds) 1000 -}}
{{- if ge (int .Values.config.shutdownTimeoutMs) $graceMs -}}
{{- fail (printf "config.shutdownTimeoutMs (%d) must be less than deployment.terminationGracePeriodSeconds (%ds), otherwise Kubernetes sends SIGKILL before graceful shutdown finishes and in-flight requests are cut off." (int .Values.config.shutdownTimeoutMs) (int .Values.deployment.terminationGracePeriodSeconds)) -}}
{{- end -}}
{{- end -}}
{{- end }}
{{/*
The public origin, with the base path removed so that it can be concatenated with
a route.
*/}}
{{- define "go-bananas.publicUrl" -}}
{{- .Values.config.publicBaseUrl -}}
{{- end }}
{{/*
The base path, normalised to either an empty string or a leading-slash path with no
trailing slash.
*/}}
{{- define "go-bananas.basePath" -}}
{{- .Values.config.basePath | trimSuffix "/" -}}
{{- end }}
{{/*
Whether the deployment is served over TLS, which decides the Secure cookie flag and
whether HSTS is advertised.
*/}}
{{- define "go-bananas.isSecure" -}}
{{- if .Values.service.sessionSecure -}}true{{- end -}}
{{- end }}
{{/*
The database file path inside the container.
*/}}
{{- define "go-bananas.databasePath" -}}
{{- printf "%s/go-bananas.db" (trimSuffix "/" .Values.persistence.mountPath) -}}
{{- end }}
{{/*
The data volume name, whether it is a PVC or an emptyDir.
*/}}
{{- define "go-bananas.dataVolumeName" -}}
{{- if .Values.persistence.existingClaim -}}
{{- .Values.persistence.existingClaim -}}
{{- else -}}
{{- printf "%s-data" (include "go-bananas.fullname" .) -}}
{{- end -}}
{{- end }}

View file

@ -0,0 +1,93 @@
{{/*
The ConfigMap holding every non-secret setting.
Splitting configuration from secrets is the point of this file: a ConfigMap is
readable by anyone with namespace access and appears in `kubectl describe`, so
nothing sensitive can end up in one by accident.
*/}}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "go-bananas.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "go-bananas.labels" . | nindent 4 }}
{{- with .Values.global.extraAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
data:
# ---------------------------------------------------------------- runtime ---
BANANAS_ENVIRONMENT: {{ .Values.config.environment | quote }}
BANANAS_HTTP_ADDR: ":8080"
BANANAS_PUBLIC_BASE_URL: {{ include "go-bananas.publicUrl" . | quote }}
BANANAS_BASE_PATH: {{ include "go-bananas.basePath" . | quote }}
BANANAS_STATIC_DIR: "/srv/web"
BANANAS_SDK_DIR: "/srv/sdk"
BANANAS_LOG_LEVEL: {{ .Values.config.logLevel | quote }}
BANANAS_LOG_FORMAT: {{ .Values.config.logFormat | quote }}
# ---------------------------------------------------------------- storage ---
BANANAS_DATABASE_PATH: {{ include "go-bananas.databasePath" . | quote }}
BANANAS_DB_MAX_CONNS: {{ .Values.config.database.maxConns | quote }}
BANANAS_DB_BUSY_TIMEOUT: {{ printf "%dms" (int .Values.config.database.busyTimeoutMs) | quote }}
BANANAS_SEED_ON_START: "true"
# ------------------------------------------------------------------- http ---
BANANAS_HTTP_READ_TIMEOUT: "15s"
BANANAS_HTTP_WRITE_TIMEOUT: "30s"
BANANAS_HTTP_IDLE_TIMEOUT: "120s"
BANANAS_SHUTDOWN_TIMEOUT: {{ printf "%dms" (int .Values.config.shutdownTimeoutMs) | quote }}
BANANAS_RATE_LIMIT_PER_MINUTE: {{ .Values.config.rateLimitPerMinute | quote }}
BANANAS_ENABLE_METRICS: {{ .Values.config.enableMetrics | quote }}
BANANAS_ENABLE_PROFILER: {{ .Values.config.enableProfiler | quote }}
# Rendered as a comma-separated list because that is what the config loader
# parses. An empty value is deliberate: it means forwarding headers are ignored,
# so the audit log records the real peer rather than a spoofable header.
BANANAS_TRUSTED_PROXIES: {{ join "," .Values.config.trustedProxies | quote }}
BANANAS_CORS_ALLOWED_ORIGINS: {{ join "," .Values.config.corsAllowedOrigins | quote }}
# ----------------------------------------------------------------- runtime ---
BANANAS_RUNTIME_PUBLIC: "true"
BANANAS_RUNTIME_CACHE_CONTROL: "public, max-age=60, s-maxage=300, stale-while-revalidate=86400"
# Set this when assets are served from a CDN, so the manifest hands consumers the
# CDN URL rather than this service.
BANANAS_RUNTIME_CDN_BASE_URL: {{ .Values.config.runtimeCdnBaseUrl | default "" | quote }}
# ------------------------------------------------------------- identity ---
BANANAS_AUTH_MODE: {{ .Values.identity.mode | quote }}
{{- if eq .Values.identity.mode "oidc" }}
BANANAS_OIDC_ISSUER: {{ .Values.identity.oidc.issuer | quote }}
BANANAS_OIDC_CLIENT_ID: {{ .Values.identity.oidc.clientId | quote }}
BANANAS_OIDC_SCOPES: {{ join "," .Values.identity.oidc.scopes | quote }}
BANANAS_OIDC_REDIRECT_URL: {{ .Values.identity.oidc.redirectURL | default "" | quote }}
BANANAS_OIDC_POST_LOGOUT_REDIRECT_URL: {{ .Values.identity.oidc.postLogoutRedirectURL | default "" | quote }}
{{- with .Values.identity.oidc.adminGroups }}
BANANAS_OIDC_ADMIN_GROUPS: {{ join "," . | quote }}
{{- end }}
{{- with .Values.identity.oidc.editorGroups }}
BANANAS_OIDC_EDITOR_GROUPS: {{ join "," . | quote }}
{{- end }}
{{- with .Values.identity.oidc.viewerGroups }}
BANANAS_OIDC_VIEWER_GROUPS: {{ join "," . | quote }}
{{- end }}
{{- if .Values.identity.oidc.skipTLSVerify }}
BANANAS_OIDC_SKIP_TLS_VERIFY: "true"
{{- end }}
{{- end }}
{{- if eq .Values.identity.mode "dev" }}
# Development bypass: every request is a local administrator. The application
# refuses to start in this mode outside a development environment, so this block
# is refused by config.environment: production above.
BANANAS_DEV_FALLBACK_ROLE: "admin"
{{- end }}
# ----------------------------------------------------------------- session ---
BANANAS_SESSION_COOKIE_NAME: "go-bananas-session"
BANANAS_SESSION_TTL: {{ printf "%dh" (int .Values.config.sessionTTLHours) | quote }}
BANANAS_SESSION_SECURE: {{ .Values.service.sessionSecure | quote }}
BANANAS_SESSION_SAMESITE: {{ .Values.service.sessionSameSite | quote }}

View file

@ -0,0 +1,218 @@
{{- include "go-bananas.validate" . }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "go-bananas.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "go-bananas.labels" . | nindent 4 }}
app.kubernetes.io/component: server
{{- with .Values.global.extraAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.deployment.replicaCount }}
{{- end }}
revisionHistoryLimit: {{ .Values.deployment.revisionHistoryLimit }}
minReadySeconds: {{ .Values.deployment.minReadySeconds }}
progressDeadlineSeconds: {{ .Values.deployment.progressDeadlineSeconds }}
strategy:
{{- toYaml .Values.deployment.strategy | nindent 4 }}
selector:
matchLabels:
{{- include "go-bananas.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
# Rolling the pods when the ConfigMap changes is what makes `helm upgrade`
# actually apply a configuration change: without this annotation the
# Deployment spec is unchanged and Kubernetes would not restart anything.
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- if and .Values.persistence.enabled (eq .Values.persistence.type "persistentVolumeClaim") (not .Values.persistence.existingClaim) (gt (int .Values.deployment.replicaCount) 1) }}
# A ReadWriteOnce volume can be mounted by one pod at a time. With more than
# one replica the others will fail to start unless the storage class
# supports concurrent mounts. Set deployment.replicaCount to 1, or use a
# ReadWriteMany filesystem.
go-bananas.urpi.be/storage-note: >-
ReadWriteOnce volume with {{ .Values.deployment.replicaCount }} replicas.
Only one replica can mount it unless the storage class permits concurrent
mounts.
{{- end }}
{{- if and (eq .Values.identity.mode "oidc") .Values.identity.oidc.generateSecret }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- end }}
{{- with .Values.deployment.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "go-bananas.selectorLabels" . | nindent 8 }}
{{- with .Values.deployment.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.image.pullPolicy }}
imagePullPolicy: {{ . }}
{{- end }}
serviceAccountName: {{ include "go-bananas.serviceAccountName" . }}
automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.deployment.priorityClassName }}
priorityClassName: {{ . }}
{{- end }}
{{- with .Values.deployment.hostAliases }}
hostAliases:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
terminationGracePeriodSeconds: {{ .Values.deployment.terminationGracePeriodSeconds }}
{{- if .Values.deployment.lifecycle.preStop.enabled }}
# A preStop sleep gives the Service endpoints controller time to remove this
# pod from the endpoint list before the process starts refusing connections.
# Without it, a rolling update can drop requests that were already routed.
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep {{ .Values.deployment.lifecycle.preStop.sleepSeconds }}"]
{{- end }}
containers:
- name: go-bananas
image: {{ include "go-bananas.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- with .Values.deployment.extraArgs }}
args:
{{- toYaml . | nindent 12 }}
{{- end }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
envFrom:
- configMapRef:
name: {{ include "go-bananas.fullname" . }}
{{- if ne .Values.identity.mode "disabled" }}
env:
{{- /*
Secrets are injected through env rather than a mounted volume. Both
are equally protected by the kubelet; env avoids the temptation to
read the value from a file path into a log line, and the application
never echoes it back.
*/}}
- name: BANANAS_SESSION_SECRET
valueFrom:
secretKeyRef:
name: {{ include "go-bananas.secretName" . }}
key: {{ include "go-bananas.sessionSecretKey" . }}
optional: false
{{- if and (eq .Values.identity.mode "oidc") (not (empty (include "go-bananas.usesExistingSecret" .))) }}
- name: BANANAS_OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: {{ include "go-bananas.secretName" . }}
key: {{ include "go-bananas.clientSecretKey" . }}
optional: false
{{- end }}
{{- with .Values.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
ports:
- name: http
containerPort: 8080
protocol: TCP
{{- if .Values.probes.startup.enabled }}
startupProbe:
httpGet:
path: {{ .Values.probes.startup.path }}
port: http
periodSeconds: {{ .Values.probes.startup.periodSeconds }}
failureThreshold: {{ .Values.probes.startup.failureThreshold }}
{{- end }}
{{- if .Values.probes.liveness.enabled }}
# Liveness deliberately uses /healthz, which does not touch the database.
# A probe that fails on a slow database would have the kubelet restart a
# process that is otherwise healthy, turning a dependency blip into an
# outage.
livenessProbe:
httpGet:
path: {{ .Values.probes.liveness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
failureThreshold: {{ .Values.probes.liveness.failureThreshold }}
{{- end }}
{{- if .Values.probes.readiness.enabled }}
# Readiness does check the database: a pod that cannot reach it cannot
# serve anything, and removing it from the endpoints is the correct
# response.
readinessProbe:
httpGet:
path: {{ .Values.probes.readiness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
failureThreshold: {{ .Values.probes.readiness.failureThreshold }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: data
mountPath: {{ .Values.persistence.mountPath }}
{{- /*
The root filesystem is read-only, so /tmp has to be an explicit
emptyDir. Go writes temporary files there during compression and
the SQLite driver uses it for spill, so without this the process
fails in ways that look unrelated.
*/}}
- name: tmp
mountPath: /tmp
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumes:
- name: data
{{- if not .Values.persistence.enabled }}
emptyDir: {}
{{- else if .Values.persistence.existingClaim }}
persistentVolumeClaim:
claimName: {{ .Values.persistence.existingClaim }}
readOnly: false
{{- else if eq .Values.persistence.type "persistentVolumeClaim" }}
persistentVolumeClaim:
claimName: {{ include "go-bananas.dataVolumeName" . }}
readOnly: false
{{- else }}
emptyDir: {}
{{- end }}
- name: tmp
emptyDir:
medium: Memory
sizeLimit: 32Mi
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.deployment.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.deployment.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.deployment.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.deployment.topologySpreadConstraints }}
topologySpreadConstraints:
{{- toYaml . | nindent 8 }}
{{- end }}

View file

@ -0,0 +1,145 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "go-bananas.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "go-bananas.labels" . | nindent 4 }}
{{- with .Values.global.extraAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "go-bananas.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- with .Values.autoscaling.behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- if .Values.podDisruptionBudget.enabled }}
---
{{/*
A PodDisruptionBudget only earns its keep if it does not also block a node drain.
minAvailable of 1 with two replicas permits exactly one disruption, which is the
useful setting: a drain can proceed, but not all pods at once.
*/}}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "go-bananas.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "go-bananas.labels" . | nindent 4 }}
{{- with .Values.global.extraAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.podDisruptionBudget.maxUnavailable }}
maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
{{- else }}
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
{{- end }}
{{- with .Values.podDisruptionBudget.unhealthyPodEvictionPolicy }}
unhealthyPodEvictionPolicy: {{ . }}
{{- end }}
selector:
matchLabels:
{{- include "go-bananas.selectorLabels" . | nindent 6 }}
{{- end }}
{{- if .Values.networkPolicy.enabled }}
---
{{/*
Default-deny ingress with explicit allowances.
The default posture is deny: the administration API performs writes, so a cluster
where every pod can reach it means every pod can change every theme. The two
allowances are the ingress controller, which is how real users arrive, and the
metrics paths, so Prometheus can still scrape.
*/}}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "go-bananas.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "go-bananas.labels" . | nindent 4 }}
{{- with .Values.global.extraAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
podSelector:
matchLabels:
{{- include "go-bananas.selectorLabels" . | nindent 6 }}
policyTypes:
- Ingress
ingress:
{{- if .Values.networkPolicy.allowedNamespaces }}
- from:
{{- range .Values.networkPolicy.allowedNamespaces }}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . }}
{{- end }}
ports:
- protocol: TCP
port: 8080
{{- else }}
# An empty allowedNamespaces list means the cluster's ingress controllers are
# allowed, identified by their well-known labels rather than by a namespace
# name, which differs between distributions.
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: TCP
port: 8080
{{- end }}
{{- if .Values.config.enableMetrics }}
- from:
{{- if .Values.networkPolicy.allowedNamespaces }}
{{- range .Values.networkPolicy.allowedNamespaces }}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . }}
{{- end }}
{{- else }}
- namespaceSelector: {}
{{- end }}
ports:
- protocol: TCP
port: 8080
{{- end }}
{{- end }}
{{- with .Values.extraManifests }}
---
{{ toYaml . }}
{{- end }}

View file

@ -0,0 +1,43 @@
{{- if .Values.ingress.enabled }}
{{- $fullName := include "go-bananas.fullname" . }}
{{- $servicePort := .Values.service.port }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ $fullName }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "go-bananas.labels" . | nindent 4 }}
{{- if or .Values.ingress.annotations .Values.global.extraAnnotations }}
annotations:
{{- with .Values.global.extraAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.ingress.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
spec:
{{- with .Values.ingress.className }}
ingressClassName: {{ . }}
{{- end }}
{{- with .Values.ingress.tls }}
tls:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType | default "Prefix" }}
backend:
service:
name: {{ $fullName }}
port:
number: {{ $servicePort }}
{{- end }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,161 @@
{{- if .Values.persistence.enabled }}
{{- if eq .Values.persistence.type "persistentVolumeClaim" }}
{{- if not .Values.persistence.existingClaim }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "go-bananas.dataVolumeName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "go-bananas.labels" . | nindent 4 }}
annotations:
{{- /*
Retain the volume when the release is uninstalled. The themes in it are the
only copy of an operator's work: deleting a volume on uninstall would make
`helm uninstall` destructive in a way that is very easy to do by accident.
*/}}
helm.sh/resource-policy: keep
{{- with .Values.global.extraAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
- {{ .Values.persistence.accessMode }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- with .Values.persistence.storageClassName }}
storageClassName: {{ . | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- if and .Values.persistence.snapshot.enabled (eq .Values.persistence.type "persistentVolumeClaim") }}
---
{{/*
A scheduled SQLite snapshot.
Three things this gets right that the obvious implementation does not:
1. *It is a CronJob, not a Helm hook.* Combining `schedule` with
`helm.sh/hook: pre-upgrade` is a contradiction: a hook runs on every upgrade
regardless of the schedule, and with `hook-succeeded` the resource is then
deleted. As a hook it would both ignore its own schedule and never produce
anything to restore.
2. *The snapshots land on the same volume.* An `emptyDir` destination would be
discarded the moment the pod exits, which makes the whole exercise pointless.
3. *It runs the service's own `backup` command.* Copying a database file while a
write is in flight produces a torn copy. `go-bananas backup` uses SQLite's
VACUUM INTO, which reads through the database's own backup machinery and is
consistent without blocking writers. Using the application binary rather than a
`sqlite3` from some other image also guarantees the SQLite library is the same
version the schema was written for, and means no third-party image and no
package installation into a read-only root filesystem.
The known limitation is stated rather than hidden: the job mounts the same
ReadWriteOnce volume as the Deployment, so on a cluster whose CSI driver enforces
single-node attachment strictly the job can fail to mount while the application is
running. If your cluster reports MultiAttach errors, give the job a node affinity
that pins it alongside the running pod, or take snapshots from outside the cluster.
A release using `emptyDir` data has nothing to snapshot and is unaffected.
Restoring is manual, because a restore should be a deliberate act:
kubectl -n <namespace> exec <pod> -- /usr/local/bin/go-bananas backup --to /tmp/check.db
# then stop the service, replace the database file, and start it again.
*/}}
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "go-bananas.fullname" . }}-snapshot
namespace: {{ .Release.Namespace }}
labels:
{{- include "go-bananas.labels" . | nindent 4 }}
annotations:
{{- with .Values.global.extraAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
schedule: {{ .Values.persistence.snapshot.schedule | quote }}
concurrencyPolicy: Forbid
# Snapshots are named with a timestamp, so two runs cannot collide, and an
# overlap is a sign the volume is too slow rather than a problem to solve by
# running them at once.
startingDeadlineSeconds: 600
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
metadata:
labels:
{{- include "go-bananas.labels" . | nindent 8 }}
spec:
backoffLimit: 2
template:
metadata:
labels:
{{- include "go-bananas.selectorLabels" . | nindent 12 }}
app.kubernetes.io/component: snapshot
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
containers:
- name: snapshot
# The service image itself: the snapshot command is part of the
# binary, so there is no second image to keep in step with this one.
image: {{ include "go-bananas.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
env:
- name: DATABASE_PATH
value: {{ include "go-bananas.databasePath" . | quote }}
- name: SNAPSHOT_RETENTION_DAYS
value: {{ .Values.persistence.snapshot.retention | quote }}
# A shell, because the snapshot filename needs a timestamp and an argv
# list cannot expand one.
command: ["/bin/sh", "-euc"]
args:
- |
dest="/snapshot/go-bananas-$(date -u +%Y%m%dT%H%M%SZ).db"
/usr/local/bin/go-bananas backup --to "$dest"
# Retention by age, run here rather than as a lifecycle hook so it
# happens on the same schedule as the snapshot and cannot be skipped
# by a container that failed to start. An old snapshot is removed even
# though the Job that produced it has long since been garbage
# collected.
find /snapshot -name 'go-bananas-*.db' \
-mtime "+$SNAPSHOT_RETENTION_DAYS" -delete
ls -la /snapshot
volumeMounts:
- name: data
mountPath: {{ .Values.persistence.mountPath }}
readOnly: true
- name: snapshot
# The writable half of the same volume. Only the snapshot
# directory is writable; the database file itself is mounted
# read-only above.
mountPath: /snapshot
subPath: snapshots
volumes:
- name: data
persistentVolumeClaim:
claimName: {{ .Values.persistence.existingClaim | default (include "go-bananas.dataVolumeName" .) }}
readOnly: true
- name: snapshot
persistentVolumeClaim:
claimName: {{ .Values.persistence.existingClaim | default (include "go-bananas.dataVolumeName" .) }}
# Writable, and only for the subPath above. The pod as a whole can
# still reach the database, which is why the read-only mount and the
# `runAsNonRoot` security context are both kept.
readOnly: false
{{- end }}
{{- end }}

View file

@ -0,0 +1,63 @@
{{/*
The Secret holding the session signing key and, unless an existing Secret is
referenced, the OIDC client secret.
Two things are deliberate here:
* `lookup` is used to read an already-generated value back on upgrade. Without it,
every `helm upgrade` would rotate the session secret and silently sign every
user out. The client secret is not looked up: rotating a client secret is
occasionally the intent, and an operator who wants to keep it uses
`existingSecret`.
* Values are base64-encoded, not stringData, because stringData is transformed by
the API server and would make the stored manifest differ from what Helm holds,
which in turn makes the upgrade detection above unreliable.
*/}}
{{- if and (eq .Values.identity.mode "oidc") .Values.identity.oidc.generateSecret }}
{{- $secretName := include "go-bananas.secretName" . }}
{{- $sessionKey := include "go-bananas.sessionSecretKey" . }}
{{- $clientKey := include "go-bananas.clientSecretKey" . }}
{{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName }}
apiVersion: v1
kind: Secret
metadata:
name: {{ $secretName }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "go-bananas.labels" . | nindent 4 }}
annotations:
# A hook annotation would be tempting, but a Secret must exist before the
# Deployment starts and a pre-install hook cannot guarantee ordering with a
# plain Deployment.
helm.sh/resource-policy: keep
{{- with .Values.global.extraAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
type: Opaque
data:
{{- /* Reuse the previous session secret so an upgrade does not sign everyone out. */}}
{{- $session := "" }}
{{- if $existing }}
{{- $session = (index $existing.data $sessionKey | default "") }}
{{- end }}
{{- if $session }}
{{ $sessionKey }}: {{ $session }}
{{- else }}
{{ $sessionKey }}: {{ randAlphaNum 64 | b64enc }}
{{- end }}
{{- /*
The OIDC client secret is only generated when one was not supplied out of
band. Leaving it empty produces a deployment that fails to authenticate, which
the application reports clearly at start-up rather than as a login loop.
*/}}
{{- $client := "" }}
{{- if $existing }}
{{- $client = (index $existing.data $clientKey | default "") }}
{{- end }}
{{- if $client }}
{{ $clientKey }}: {{ $client }}
{{- else }}
{{ $clientKey }}: {{ "" | b64enc }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,64 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "go-bananas.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "go-bananas.labels" . | nindent 4 }}
app.kubernetes.io/component: server
{{- if or .Values.service.annotations .Values.global.extraAnnotations }}
annotations:
{{- with .Values.global.extraAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.service.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
spec:
type: {{ .Values.service.type }}
{{- with .Values.service.clusterIP }}
clusterIP: {{ . }}
{{- end }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
{{- if and (eq .Values.service.type "NodePort") .Values.service.nodePort }}
nodePort: {{ .Values.service.nodePort }}
{{- end }}
selector:
{{- include "go-bananas.selectorLabels" . | nindent 4 }}
{{- if and (eq .Values.persistence.type "persistentVolumeClaim") (gt (int .Values.deployment.replicaCount) 1) }}
# More than one replica against a ReadWriteOnce volume needs a rollout strategy
# that does not overlap the two mounts. The values validator warns about this
# combination; this annotation records the intent in the live object.
sessionAffinity: None
{{- end }}
{{- with .Values.service.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- if and .Values.metrics.serviceMonitor.enabled .Values.config.enableMetrics }}
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "go-bananas.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "go-bananas.labels" . | nindent 4 }}
{{- with .Values.metrics.serviceMonitor.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
selector:
matchLabels:
{{- include "go-bananas.selectorLabels" . | nindent 6 }}
endpoints:
- port: http
path: /metrics
interval: {{ .Values.metrics.serviceMonitor.interval }}
scrapeTimeout: {{ .Values.metrics.serviceMonitor.scrapeTimeout }}
honorLabels: {{ .Values.metrics.serviceMonitor.honorLabels }}
{{- end }}

View file

@ -0,0 +1,56 @@
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "go-bananas.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "go-bananas.labels" . | nindent 4 }}
{{- if or .Values.serviceAccount.annotations .Values.global.extraAnnotations }}
annotations:
{{- with .Values.global.extraAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.serviceAccount.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- /*
The API token is not mounted. The service talks to nothing inside the cluster:
its database is a local file and its identity provider is external. Mounting a
token it does not need would be a credential available to any container escape.
*/}}
automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }}
{{- end }}
{{- if .Values.rbac.create }}
---
apiVersion: rbac.authoriz.k8s.io/v1
kind: Role
metadata:
name: {{ include "go-bananas.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "go-bananas.labels" . | nindent 4 }}
{{- with .Values.global.extraAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
{{- toYaml .Values.rbac.rules | nindent 2 }}
---
apiVersion: rbac.authoriz.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "go-bananas.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "go-bananas.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "go-bananas.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "go-bananas.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- end }}

View file

@ -0,0 +1,44 @@
# Dev values for go-bananas
nameOverride: ""
fullnameOverride: ""
replicaCount: 1
image:
repository: nexus.urpi.be/docker-hosted/go-bananas
tag: "0.0.1"
pullPolicy: Always
service:
type: ClusterIP
port: 80
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: bananas-dev.intern-urpi.be
paths:
- path: /
pathType: Prefix
tls:
- secretName: go-bananas-tls
hosts:
- bananas-dev.intern-urpi.be
certManager:
enabled: true
issuerRef:
name: letsencrypt-staging
kind: ClusterIssuer
resources:
limits:
cpu: 300m
memory: 256Mi
requests:
cpu: 50m
memory: 128Mi

View file

@ -0,0 +1,51 @@
# Local (LCL) values for go-bananas.
# Served at https://bananas-lcl.intern-urpi.be by the docker TLS edge.
#
# NOTE: this chart validates values against values.schema.json, which allows
# only the keys defined there -- no certManager and no replicaCount here
# (values-dev.yaml still carries both and fails `helm template`).
nameOverride: ""
fullnameOverride: ""
image:
registry: localhost:5000
repository: go-bananas
tag: "0.0.1"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
# Local environment: development unlocks the local authentication bypass, so
# the deployment works without the shared Keycloak realm at auth-dev.urpi.be
# (go-loose does not serve /realms/... endpoints, so the oidc default cannot
# be pointed at it either).
config:
environment: development
publicBaseUrl: "https://bananas-lcl.intern-urpi.be"
identity:
mode: dev
ingress:
enabled: true
className: traefik
annotations: {}
hosts:
- host: bananas-lcl.intern-urpi.be
paths:
- path: /
pathType: Prefix
tls:
- secretName: local-dev-cert
hosts:
- bananas-lcl.intern-urpi.be
resources:
limits:
cpu: 300m
memory: 256Mi
requests:
cpu: 50m
memory: 128Mi

View file

@ -0,0 +1,59 @@
# PRD values for go-bananas
global:
imagePullSecrets:
- name: nexus-registry
image:
registry: nexus.urpi.be
repository: docker-hosted/go-bananas
tag: "dev-1791360078"
pullPolicy: Always
deployment:
replicaCount: 1
service:
type: ClusterIP
port: 80
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: bananas.urpi.be
paths:
- path: /
pathType: Prefix
tls:
- secretName: go-bananas-tls
hosts:
- bananas.urpi.be
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
config:
environment: production
publicBaseUrl: "https://bananas.urpi.be"
identity:
mode: oidc
oidc:
issuer: "https://auth.urpi.be/realms/urpi"
clientId: "go-bananas"
existingSecret: "go-bananas-auth"
generateSecret: false
scopes:
- profile
- email
- groups
adminGroups: ["go-bananas-admin"]
editorGroups: ["go-bananas-editor"]

View file

@ -0,0 +1,47 @@
# PRD values for go-bananas
nameOverride: ""
fullnameOverride: ""
replicaCount: 1
imagePullSecrets:
- name: nexus-registry
image:
repository: nexus.urpi.be/docker-hosted/go-bananas
tag: "0.0.1"
pullPolicy: Always
service:
type: ClusterIP
port: 80
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: bananas.urpi.be
paths:
- path: /
pathType: Prefix
tls:
- secretName: go-bananas-tls
hosts:
- bananas.urpi.be
certManager:
enabled: true
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi

View file

@ -0,0 +1,44 @@
# TST values for go-bananas
nameOverride: ""
fullnameOverride: ""
replicaCount: 1
image:
repository: nexus.urpi.be/docker-hosted/go-bananas
tag: "0.0.1"
pullPolicy: Always
service:
type: ClusterIP
port: 80
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: bananas-tst.intern-urpi.be
paths:
- path: /
pathType: Prefix
tls:
- secretName: go-bananas-tls
hosts:
- bananas-tst.intern-urpi.be
certManager:
enabled: true
issuerRef:
name: letsencrypt-staging
kind: ClusterIssuer
resources:
limits:
cpu: 300m
memory: 256Mi
requests:
cpu: 50m
memory: 128Mi

View file

@ -0,0 +1,338 @@
{
"$schema": "https://json-schema.org/draft-07/schema#",
"title": "Go Bananas!! Helm values",
"description": "Validation schema for the Go Bananas!! chart. Editors use this to offer completion and to catch a mistyped key before the chart is rendered.",
"type": "object",
"additionalProperties": false,
"required": ["image", "identity", "config", "service"],
"properties": {
"global": {
"type": "object",
"properties": {
"imageRegistry": { "type": "string" },
"imagePullSecrets": {
"type": "array",
"items": {
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
}
},
"extraLabels": { "type": "object" },
"extraAnnotations": { "type": "object" }
}
},
"nameOverride": { "type": "string", "maxLength": 63 },
"fullnameOverride": { "type": "string", "maxLength": 63 },
"image": {
"type": "object",
"additionalProperties": false,
"required": ["repository"],
"properties": {
"registry": { "type": "string" },
"repository": { "type": "string", "minLength": 1 },
"tag": { "type": "string" },
"digest": { "type": "string" },
"pullPolicy": { "enum": ["Always", "IfNotPresent", "Never"] }
}
},
"deployment": {
"type": "object",
"additionalProperties": false,
"properties": {
"replicaCount": { "type": "integer", "minimum": 0, "maximum": 100 },
"extraArgs": { "type": "array", "items": { "type": "string" } },
"revisionHistoryLimit": { "type": "integer", "minimum": 0 },
"minReadySeconds": { "type": "integer", "minimum": 0 },
"progressDeadlineSeconds": { "type": "integer", "minimum": 1 },
"terminationGracePeriodSeconds": { "type": "integer", "minimum": 0 },
"strategy": { "type": "object" },
"podAnnotations": { "type": "object" },
"podLabels": { "type": "object" },
"hostAliases": { "type": "array" },
"nodeSelector": { "type": "object" },
"tolerations": { "type": "array" },
"affinity": { "type": "object" },
"topologySpreadConstraints": { "type": "array" },
"priorityClassName": { "type": "string" },
"lifecycle": {
"type": "object",
"properties": {
"preStop": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"sleepSeconds": { "type": "integer", "minimum": 0, "maximum": 60 }
}
}
}
}
}
},
"resources": { "type": "object" },
"config": {
"type": "object",
"additionalProperties": false,
"required": ["environment", "publicBaseUrl"],
"properties": {
"environment": {
"type": "string",
"minLength": 1,
"description": "The deployment name. Anything other than a development or test value disables the dev authentication bypass."
},
"logLevel": { "enum": ["debug", "info", "warn", "error"] },
"logFormat": { "enum": ["json", "text"] },
"publicBaseUrl": {
"type": "string",
"pattern": "^https?://[^/]+",
"description": "The externally reachable origin, with no trailing slash."
},
"basePath": {
"type": "string",
"pattern": "^$|^/",
"description": "A route prefix for a path-based ingress. Empty or a leading-slash path."
},
"rateLimitPerMinute": { "type": "integer", "minimum": 0 },
"trustedProxies": { "type": "array", "items": { "type": "string" } },
"corsAllowedOrigins": { "type": "array", "items": { "type": "string" } },
"enableMetrics": { "type": "boolean" },
"enableProfiler": { "type": "boolean" },
"shutdownTimeoutMs": { "type": "integer", "minimum": 0, "maximum": 300000 },
"runtimeCdnBaseUrl": { "type": "string" },
"database": {
"type": "object",
"additionalProperties": false,
"properties": {
"maxConns": { "type": "integer", "minimum": 1, "maximum": 128 },
"busyTimeoutMs": { "type": "integer", "minimum": 0, "maximum": 60000 }
}
},
"sessionTTLHours": { "type": "integer", "minimum": 1, "maximum": 720 }
}
},
"persistence": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"type": { "enum": ["persistentVolumeClaim", "emptyDir"] },
"accessMode": {
"enum": ["ReadWriteOnce", "ReadWriteMany", "ReadWriteOncePod", "ReadOnlyMany"]
},
"size": { "type": "string", "pattern": "^[0-9]+(Ei|Pi|Ti|Gi|Mi|Ki|E|P|T|G|M|K)?$" },
"storageClassName": { "type": "string" },
"existingClaim": { "type": "string" },
"mountPath": { "type": "string", "pattern": "^/" },
"snapshot": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"schedule": { "type": "string", "minLength": 1 },
"retention": { "type": "integer", "minimum": 1, "maximum": 365 }
}
}
}
},
"service": {
"type": "object",
"additionalProperties": false,
"properties": {
"type": { "enum": ["ClusterIP", "NodePort", "LoadBalancer"] },
"port": { "type": "integer", "minimum": 1, "maximum": 65535 },
"nodePort": { "type": ["integer", "string"] },
"clusterIP": { "type": "string" },
"sessionSecure": { "type": "boolean" },
"sessionSameSite": { "enum": ["Lax", "Strict", "None"] },
"annotations": { "type": "object" },
"labels": { "type": "object" }
}
},
"ingress": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"className": { "type": "string" },
"annotations": { "type": "object" },
"hosts": {
"type": "array",
"items": {
"type": "object",
"required": ["host"],
"properties": {
"host": { "type": "string", "minLength": 1 },
"paths": {
"type": "array",
"items": {
"type": "object",
"properties": {
"path": { "type": "string", "pattern": "^/" },
"pathType": { "enum": ["Prefix", "Exact", "ImplementationSpecific"] }
}
}
}
}
}
},
"tls": {
"type": "array",
"items": {
"type": "object",
"required": ["secretName"],
"properties": {
"secretName": { "type": "string" },
"hosts": { "type": "array", "items": { "type": "string" } }
}
}
}
}
},
"identity": {
"type": "object",
"additionalProperties": false,
"required": ["mode"],
"properties": {
"mode": {
"enum": ["oidc", "dev", "disabled"],
"description": "oidc is the production choice. dev and disabled are refused by the application outside a development environment."
},
"sessionSecretKey": { "type": "string", "minLength": 1 },
"oidc": {
"type": "object",
"additionalProperties": false,
"properties": {
"issuer": { "type": "string", "pattern": "^https?://" },
"clientId": { "type": "string" },
"clientSecretKey": { "type": "string", "minLength": 1 },
"existingSecret": { "type": "string" },
"existingSecretKeys": { "type": "object" },
"generateSecret": { "type": "boolean" },
"scopes": { "type": "array", "items": { "type": "string" } },
"adminGroups": { "type": "array", "items": { "type": "string" } },
"editorGroups": { "type": "array", "items": { "type": "string" } },
"viewerGroups": { "type": "array", "items": { "type": "string" } },
"redirectURL": { "type": "string" },
"postLogoutRedirectURL": { "type": "string" },
"logoutURL": { "type": "string" },
"skipTLSVerify": { "type": "boolean" },
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 300 }
}
}
}
},
"probes": {
"type": "object",
"additionalProperties": false,
"properties": {
"liveness": { "$ref": "#/definitions/probe" },
"readiness": { "$ref": "#/definitions/probe" },
"startup": { "$ref": "#/definitions/probe" }
}
},
"autoscaling": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"minReplicas": { "type": "integer", "minimum": 1 },
"maxReplicas": { "type": "integer", "minimum": 1 },
"targetCPUUtilizationPercentage": { "type": "integer", "minimum": 1, "maximum": 100 },
"targetMemoryUtilizationPercentage": { "type": "integer", "minimum": 0, "maximum": 100 },
"behavior": { "type": "object" }
}
},
"podDisruptionBudget": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"minAvailable": { "type": ["integer", "string"] },
"maxUnavailable": { "type": ["integer", "string"] },
"unhealthyPodEvictionPolicy": { "enum": ["IfHealthyBudget", "AlwaysAllow"] }
}
},
"serviceAccount": {
"type": "object",
"additionalProperties": false,
"properties": {
"create": { "type": "boolean" },
"name": { "type": "string" },
"annotations": { "type": "object" },
"automountServiceAccountToken": { "type": "boolean" }
}
},
"rbac": {
"type": "object",
"additionalProperties": false,
"properties": {
"create": { "type": "boolean" },
"rules": { "type": "array" }
}
},
"networkPolicy": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"allowedNamespaces": { "type": "array", "items": { "type": "string" } },
"metricsPaths": { "type": "array", "items": { "type": "string" } }
}
},
"podSecurityContext": { "type": "object" },
"securityContext": { "type": "object" },
"extraEnv": { "type": "array" },
"extraVolumes": { "type": "array" },
"extraVolumeMounts": { "type": "array" },
"metrics": {
"type": "object",
"additionalProperties": false,
"properties": {
"serviceMonitor": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"interval": { "type": "string" },
"scrapeTimeout": { "type": "string" },
"labels": { "type": "object" },
"honorLabels": { "type": "boolean" }
}
}
}
},
"extraManifests": { "type": "array" }
},
"definitions": {
"probe": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"path": { "type": "string", "pattern": "^/" },
"initialDelaySeconds": { "type": "integer", "minimum": 0 },
"periodSeconds": { "type": "integer", "minimum": 1 },
"timeoutSeconds": { "type": "integer", "minimum": 1 },
"failureThreshold": { "type": "integer", "minimum": 1 }
}
}
}
}

View file

@ -0,0 +1,481 @@
# Go Bananas!! Helm values
#
# Every value has a safe default so that `helm install` works with no arguments at
# all. The exceptions are the authentication credentials, which are required when
# auth.mode is oidc and are supplied through an existing Secret.
#
# The defaults are tuned for a small internal deployment. Before running this in
# production, read the annotated sections: replica count, the ingress, the resource
# requests and the persistence choice all deserve a deliberate decision.
# ------------------------------------------------------------------ global ---
global:
# imageRegistry overrides the registry for every image the chart pulls. Used by a
# cluster with a mirrored registry or an air-gapped pull-through cache.
imageRegistry: ""
# imagePullSecrets are added to every ServiceAccount.
imagePullSecrets:
- name: nexus-registry
# extraLabels are added to every object the chart creates, for cost allocation
# and for a Prometheus `release` label if the cluster does not add one.
extraLabels: {}
# extraAnnotations are added to every object.
extraAnnotations: {}
# ------------------------------------------------------------------- image ---
image:
registry: nexus.urpi.be
repository: docker-hosted/go-bananas
# tag defaults to the chart's appVersion, so upgrading the chart upgrades the
# image. Set it to an immutable digest for a reproducible deployment.
tag: "0.0.1"
# digest pins the image by content. Takes precedence over tag when set, which is
# the strongest guarantee that a rollout is running exactly what was reviewed.
digest: ""
pullPolicy: IfNotPresent
# --------------------------------------------------------------- deployment ---
deployment:
# Two replicas by default: the service is stateless apart from the SQLite file,
# so horizontal scaling is only safe when persistence is a shared volume or a
# network filesystem. See the persistence section.
replicaCount: 2
# extraArgs are appended to the container command. The binary takes no positional
# arguments; this exists for future flags without a chart change.
extraArgs: []
# revisionHistoryLimit keeps the last N ReplicaSets so a rollback is possible
# without the Deployment controller's default of ten consuming etcd history.
revisionHistoryLimit: 5
# minReadySeconds makes a new pod count as ready only after it has been healthy
# for this long, which stops a rolling update from proceeding on the strength of
# a single lucky probe.
minReadySeconds: 10
# progressDeadlineSeconds aborts a rollout that cannot complete, rather than
# leaving a Deployment stuck in progress indefinitely.
progressDeadlineSeconds: 600
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
podAnnotations: {}
podLabels: {}
# hostAliases is mostly useful for a development cluster where the identity
# provider is not in public DNS.
hostAliases: []
# terminationGracePeriodSeconds must exceed the application's shutdown timeout
# (config.shutdownTimeoutMs) or in-flight requests are cut off mid-response.
# The chart refuses to render when it does not.
terminationGracePeriodSeconds: 45
# A preStop sleep gives the Service endpoints controller time to remove this pod
# from the endpoint list before the process starts refusing connections.
# Without it, a rolling update drops requests that were already routed.
lifecycle:
preStop:
enabled: true
sleepSeconds: 3
# nodeSelector, tolerations, affinity and topologySpreadConstraints are passed
# through to the pod spec unchanged.
nodeSelector: {}
tolerations: []
affinity: {}
topologySpreadConstraints: []
# priorityClassName for a cluster where the API tier matters.
priorityClassName: ""
# ---------------------------------------------------------------- resources ---
resources:
requests:
cpu: 50m
memory: 96Mi
limits:
# No CPU limit is set deliberately. A CPU limit throttles the process when the
# node is busy, which for a latency-sensitive asset server produces exactly the
# tail latency it is meant to prevent. The memory limit is the one that matters:
# it is what keeps a runaway cache from taking the node down.
memory: 384Mi
# ------------------------------------------------------------------- config ---
config:
# environment is the deployment name. Anything other than development or test
# turns off the dev authentication bypass and the TLS-skip switch.
environment: production
logLevel: info
# json in production so the log is structured for a collector.
logFormat: json
# publicBaseUrl is the externally reachable origin, with no trailing slash.
# It is used to build the OIDC redirect URI, the absolute URLs in the runtime
# manifest and the HSTS decision, so getting it wrong means a login loop.
#
# The default points at a local hostname so that `helm install` works with no
# arguments. Override it for any real deployment.
publicBaseUrl: "http://themes.go-bananas.local"
# basePath prefixes every route, for deployments behind a path-based ingress
# such as https://example.com/themes/. Leave empty for the default of /.
basePath: ""
# rateLimitPerMinute caps requests per client per minute on the runtime API.
# The default is high because the legitimate pattern is one request per page
# view; the limit exists to stop abuse, not to ration traffic.
rateLimitPerMinute: 3000
# trustedProxies are the CIDRs whose X-Forwarded-For header may be believed.
# Empty means forwarding headers are ignored entirely and the direct peer is
# recorded, which is the safe default. Set this to the ingress controller's
# address or the audit log will record the ingress pod instead of the client.
trustedProxies: []
# corsAllowedOrigins are browser origins allowed to call the admin API
# cross-origin. The runtime API is public by design and is not affected.
corsAllowedOrigins: []
# enableMetrics exposes /metrics in Prometheus format.
enableMetrics: true
# enableProfiler exposes net/http/pprof. Off by default: a profiler on a
# network-reachable endpoint is a denial-of-service surface, so it is opt-in and
# should be paired with a NetworkPolicy.
enableProfiler: false
# shutdownTimeoutMs bounds graceful shutdown. Must be lower than
# deployment.terminationGracePeriodSeconds.
shutdownTimeoutMs: 25000
# runtimeCdnBaseUrl is the absolute base the runtime manifest hands to
# consumers. Set it when the stylesheets are served from a CDN or a separate
# asset host, so that consumers fetch from there instead of from this service.
runtimeCdnBaseUrl: ""
# database tuning. The defaults suit an embedded SQLite file; see docs on
# persistence before changing them.
database:
# maxConns bounds the connection pool. SQLite serialises writers, so a small
# pool is faster than a large one, not slower.
maxConns: 8
# busyTimeoutMs is how long a blocked writer waits before failing.
busyTimeoutMs: 5000
# sessionTTLHours is how long an administrative session lasts. The session is a
# signed cookie with no server-side record, so an expiry cannot be revoked
# early — see the identity section for the trade-off this represents.
sessionTTLHours: 8
# -------------------------------------------------------------- persistence ---
#
# The database is a single SQLite file. Two deployment shapes are supported:
#
# type: emptyDir A per-pod temporary file. Every theme change is lost when a
# pod restarts. Correct only for a demo or for evaluating the
# service; the built-in themes are re-seeded automatically so
# the instance still comes up, but operator work is not kept.
#
# type: persistentVolumeClaim A ReadWriteOnce volume. Correct for a
# single-replica deployment, which is the shape this chart
# recommends in production.
#
# Note on replicas: RWO volumes cannot be attached to two pods at once. With
# replicaCount > 1 and a PVC, only one pod mounts the volume successfully and the
# others crash-loop. Running more than one replica therefore requires either
# replicaCount: 1, or a filesystem that permits multiple mounts.
persistence:
enabled: true
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 2Gi
# storageClassName: "" uses the cluster default.
storageClassName: ""
# existingClaim mounts a volume the operator manages. Its lifecycle is then the
# operator's responsibility: this chart will not delete or resize it.
existingClaim: ""
mountPath: /srv/data
# A snapshot taken before an upgrade, when the upgrade involves a destructive
# change to the schema. Restoring is a manual kubectl apply.
snapshot:
enabled: false
# CronJob schedule in the cluster's timezone.
schedule: "0 3 * * *"
# How long to keep snapshots.
retention: 14
# ------------------------------------------------------------------ service ---
service:
type: ClusterIP
port: 80
# nodePort, only used when type is NodePort.
nodePort: ""
# ClusterIP: set to None for a headless service, which a StatefulSet needs.
clusterIP: ""
# The session cookie's Secure attribute and SameSite=None both require the
# browser to see the service over HTTPS. Set to true when TLS terminates at the
# ingress.
sessionSecure: true
sessionSameSite: Lax
annotations: {}
labels: {}
# ----------------------------------------------------------------- ingress ---
ingress:
enabled: false
className: nginx
annotations: {}
hosts:
- host: themes.example.com
paths:
- path: /
pathType: Prefix
# tls is empty for a plain-HTTP installation. For the usual production shape,
# point it at a cert-manager cluster issuer so the certificate is renewed
# without anyone remembering to.
tls: []
# - secretName: go-bananas-tls
# hosts:
# - themes.example.com
# ---------------------------------------------------------------- identity ---
#
# This is the part that most often goes wrong in a deployment, so it is worth being
# explicit about the three modes.
identity:
# disabled No authentication. Every request is granted administrator access.
# Refused by the application outside a development environment, so
# this cannot be deployed to production by accident.
# dev Every request is authenticated as a local administrator.
# Also refused outside development.
# oidc OpenID Connect against a real provider. The production choice.
mode: oidc
# sessionSecretKey is the key inside the Secret that holds the session signing
# secret. The secret must be at least 32 bytes.
#
# kubectl create secret generic go-bananas-auth \
# --from-literal=session-secret="$(openssl rand -base64 48)" \
# --from-literal=oidc-client-secret="..."
#
# The secret is never templated into a ConfigMap or written to a log. Rotating
# it invalidates every open session at once.
sessionSecretKey: session-secret
oidc:
# issuer is the OIDC issuer URL. Everything else — the authorization,
# token, userinfo and JWKS endpoints — is discovered from
# /.well-known/openid-configuration, so this is the only provider-specific
# value needed.
#
# The default is the shared development realm. The realm path is part of the
# issuer: this string must match the `iss` claim in the ID token exactly,
# including a trailing slash if the provider emits one. A mismatch here is the
# single most common cause of "invalid issuer" at the callback.
issuer: "https://auth-dev.urpi.be/realms/urpi"
# clientId is the client identifier registered with the provider.
clientId: "go-bananas"
# clientSecretKey names the key inside existingSecret (or generatedSecret)
# holding the client secret. Referencing it by key name rather than by value
# is what keeps the secret out of values.yaml.
clientSecretKey: oidc-client-secret
# existingSecret is a Secret you manage. Preferred for production: the chart
# will not create, rotate or delete it, and its contents never appear in a
# rendered manifest.
#
# kubectl create secret generic go-bananas-auth \
# --from-literal=session-secret="$(openssl rand -base64 48)" \
# --from-literal=oidc-client-secret="…"
existingSecret: ""
# existingSecretKeys overrides the key names inside existingSecret, for a
# Secret managed by an external secrets operator with its own naming.
existingSecretKeys: {}
# generatedSecret creates a Secret owned by this release. The session secret is
# preserved across upgrades by reading the live Secret back, so an upgrade does
# not sign every user out.
#
# The OIDC client secret cannot be generated — it has to come from the provider.
# A generated Secret therefore contains an empty client secret, which means the
# deployment will fail to authenticate until one is supplied. For anything real,
# use existingSecret.
generateSecret: true
# scopes requested in addition to openid. The email scope is what makes
# `email` appear in the session; groups is what the role mapping reads.
scopes:
- profile
- email
- groups
# Group-to-role mapping, read from the `groups` claim.
#
# The most privileged matching group wins; a user in no configured group gets
# viewer. The default is deliberately least privilege, so a mapping mistake
# fails closed rather than granting access nobody intended.
adminGroups: []
editorGroups: []
viewerGroups: []
# With all three empty, every authenticated user is a viewer. Populate these
# from the provider's actual group names before rolling this out to real users;
# the group names are realm-specific and are not guessed here.
# redirectURL overrides the computed callback. Leave empty unless the
# provider has a different redirect URI registered, which happens behind a
# path-based ingress or when several environments share a provider client.
redirectURL: ""
postLogoutRedirectURL: ""
# End-session endpoint, for providers that do not follow the RP-initiated
# logout convention.
logoutURL: ""
# skipTLSVerify disables certificate verification against the provider. It
# exists so that a development provider presenting a self-signed certificate
# can be used, and the application refuses to start with it enabled outside
# development.
skipTLSVerify: false
# timeout and insecureSkipVerify exist for the same development reason.
timeoutSeconds: 15
# ------------------------------------------------------------------- probes ---
probes:
liveness:
enabled: true
path: /healthz
initialDelaySeconds: 3
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 3
readiness:
enabled: true
path: /readyz
initialDelaySeconds: 2
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
startup:
enabled: true
path: /healthz
periodSeconds: 3
failureThreshold: 30
# --------------------------------------------------------------- autoscaling ---
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
# targetMemoryUtilizationPercentage is left unset by default: memory here is
# dominated by the render cache, which grows with the number of themes rather
# than with load, so scaling on it would respond to a content change rather than
# to traffic.
targetMemoryUtilizationPercentage: 0
behavior: {}
# ------------------------------------------------------- disruption budgets ---
#
# A PDB only helps if it does not also block a node drain. minAvailable: 1 with two
# replicas permits exactly one disruption, which is the useful setting.
podDisruptionBudget:
enabled: true
minAvailable: 1
# maxUnavailable: 1
# unhealthyPodEvictionPolicy: AlwaysAllow
# --------------------------------------------------------- service account ---
serviceAccount:
create: true
name: ""
annotations: {}
automountServiceAccountToken: false
rbac:
# create is off by default: the service talks to nothing in the cluster, so the
# default ServiceAccount needs no rules at all.
create: false
rules: []
# ------------------------------------------------------------ network policy ---
#
# A default-deny ingress with an explicit allowance for the ingress controller and
# for Prometheus. Without this, any pod in the cluster can reach the admin API.
networkPolicy:
enabled: false
# ingressNamespaces restricts which namespaces may reach the service. Empty means
# the whole cluster.
allowedNamespaces: []
# metricsPaths is the additional path allowed from the metrics sources.
metricsPaths: ["/metrics"]
# ------------------------------------------------------------------- extras ---
podSecurityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
# extraEnv is for one-off variables that do not warrant a chart value. Anything
# added here is the operator's responsibility to keep consistent across replicas.
extraEnv: []
# extraVolumes and extraVolumeMounts are for a sidecar or a config file the chart
# does not model.
extraVolumes: []
extraVolumeMounts: []
# metricsServiceMonitor creates a Prometheus Operator ServiceMonitor. Requires the
# Prometheus Operator CRDs to be installed.
metrics:
serviceMonitor:
enabled: false
interval: 30s
scrapeTimeout: 10s
labels: {}
honorLabels: true
# ------------------------------------------------------------------- extras ---
#
# `extraManifests` is an escape hatch for a resource this chart does not model. It
# is templated, so a value can reference another template's output.
extraManifests: []
# - apiVersion: v1
# kind: ConfigMap
# metadata:
# name: {{ include "go-bananas.fullname" . }}-extra
# data:
# example: "value"