Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6371f60eeb
|
No files matched your search
@@ -38,9 +38,7 @@ on:
|
||||
- searxng
|
||||
- bentopdf
|
||||
- ytdlp-web-player
|
||||
- sure
|
||||
- valkey
|
||||
# freesailarr is deliberately absent: none of its ten images takes
|
||||
# freesailarr is deliberately absent: none of its nine images takes
|
||||
# its tag from appVersion (they are all pinned in its values.yaml),
|
||||
# so this workflow would rewrite appVersion and republish the chart
|
||||
# without changing a single deployed image. Update its images through
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
# The all-charts install-test: the same job the pull-request pipeline runs, with
|
||||
# a matrix over every installable chart instead of the changed ones (Gitea #52).
|
||||
#
|
||||
# The question it answers is not "did this change break a chart" - that is the
|
||||
# pull-request pipeline's job - but "does the environment under an UNCHANGED
|
||||
# chart still hold". The install-test matrix follows the charts a pull request
|
||||
# touches, so a chart nobody has edited since the forge move carries a green
|
||||
# history from a different runner, and that green says nothing about today.
|
||||
#
|
||||
# It exists because the answer arrived once by accident: Gitea PR #48 corrected
|
||||
# a comment in 21 charts, which made every one of them "changed", and so became
|
||||
# the first all-charts run after the move - 19 of 20 matrix charts came up in
|
||||
# 12m41s in one job, and exactly one did not (rss-bridge, Gitea #51). That
|
||||
# result was worth having and nobody had planned it. This workflow makes it
|
||||
# repeatable.
|
||||
#
|
||||
# WHEN TO RUN IT: after a change to the runner, to K3S_IMAGE, to the pinned
|
||||
# KUBECTL_VERSION, to the fixtures under ci/ - and after a forge move. Not on a
|
||||
# schedule: it would spend compute every day on a question that changes rarely,
|
||||
# and a red run nobody asked for is a red run nobody reads.
|
||||
name: Install-test (all charts)
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
charts:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
description: "charts to test, separated by spaces or commas - empty means every installable chart"
|
||||
jobs:
|
||||
select-charts:
|
||||
name: "Select charts"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
charts: ${{ steps.select.outputs.charts }}
|
||||
count: ${{ steps.select.outputs.count }}
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v7
|
||||
- name: "Select the charts to test"
|
||||
id: select
|
||||
env:
|
||||
# The dispatch input is read as an environment variable and never
|
||||
# interpolated into the script body. A "${{ inputs.charts }}" inside
|
||||
# the shell would let whoever triggers the run write commands into
|
||||
# this step; it is a small door and there is no reason to leave it
|
||||
# open. The name check further down is the second half of that: these
|
||||
# values end up as a matrix entry, a k3d cluster name and an argument
|
||||
# to ci/run-install-test.sh.
|
||||
REQUESTED: ${{ inputs.charts }}
|
||||
run: |
|
||||
FAIL=0
|
||||
|
||||
# --- The candidates -------------------------------------------------
|
||||
# Empty input: every directory with a Chart.yaml, which is the same
|
||||
# definition detect-changed-charts uses for "this is a chart". No list
|
||||
# to maintain - adding a chart enrols it, the way the quantity render
|
||||
# test enrols one.
|
||||
#
|
||||
# A subset was asked for: exactly those names. This is what lets
|
||||
# someone test a single chart against the runner, which is what Gitea
|
||||
# #51 needs - and it is why an unknown or excluded name below is an
|
||||
# error rather than a silent omission. Asking for one chart and
|
||||
# getting a green run over zero charts would be the worst answer this
|
||||
# workflow could give.
|
||||
if [ -z "$(printf '%s' "$REQUESTED" | tr -d '[:space:],')" ]; then
|
||||
SUBSET=false
|
||||
CANDIDATES="$(ls -d -- */Chart.yaml 2>/dev/null | sed 's|/Chart.yaml$||' | sort)"
|
||||
echo "No subset requested - every chart directory is a candidate."
|
||||
else
|
||||
SUBSET=true
|
||||
CANDIDATES="$(printf '%s' "$REQUESTED" | tr ',' ' ' | tr -s '[:space:]' '\n' | grep -v '^$' | sort -u)"
|
||||
echo "Requested subset: $(echo "$CANDIDATES" | tr '\n' ' ')"
|
||||
fi
|
||||
|
||||
for C in $CANDIDATES; do
|
||||
case "$C" in
|
||||
*[!a-z0-9-]*)
|
||||
echo "::error::'$C' is not a chart name - chart directories in this repository are lowercase letters, digits and dashes."
|
||||
FAIL=1
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
if [ ! -f "$C/Chart.yaml" ]; then
|
||||
echo "::error::'$C' is not a chart directory of this repository (no $C/Chart.yaml)."
|
||||
FAIL=1
|
||||
fi
|
||||
done
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
|
||||
CHARTS="$(printf '%s\n' $CANDIDATES | jq -R . | jq -sc 'unique')"
|
||||
|
||||
# --- The exclusion list, through the one parser ---------------------
|
||||
# ci/run-install-test.sh --list-excluded emits "<chart><TAB><reason>"
|
||||
# and is the only thing in this repository that reads the format of
|
||||
# ci/excluded-charts.txt - here no more than in validate-charts.yml,
|
||||
# because a second parser would be a second chance to select
|
||||
# different charts, silently and in either direction (issue #231).
|
||||
# The comparison is jq's "!=" on whole strings, so a chart name is
|
||||
# never read as a pattern.
|
||||
#
|
||||
# The two callers differ in what they do with a hit, and on purpose.
|
||||
# Over all charts, an excluded chart is dropped and named with its
|
||||
# reason, exactly as in the pull-request pipeline. In a requested
|
||||
# subset it is an ERROR: someone typed that name, and quietly
|
||||
# returning a green run for a chart that was never attempted is the
|
||||
# lie #231 removed from the check list.
|
||||
SUMMARY_ROWS=""
|
||||
while IFS=$'\t' read -r EX_NAME EX_REASON; do
|
||||
[ -n "$EX_NAME" ] || continue
|
||||
if [ "$(echo "$CHARTS" | jq -r --arg c "$EX_NAME" 'index($c) != null')" = "true" ]; then
|
||||
if [ "$SUBSET" = true ]; then
|
||||
echo "::error::chart '$EX_NAME' was requested but is listed in ci/excluded-charts.txt: $EX_REASON"
|
||||
FAIL=1
|
||||
fi
|
||||
# A "|" in the justification would break out of the table cell.
|
||||
SUMMARY_ROWS="${SUMMARY_ROWS}| \`$EX_NAME\` | ${EX_REASON//|/\\|} |"$'\n'
|
||||
echo "Excluded from the install-test: $EX_NAME - $EX_REASON"
|
||||
fi
|
||||
CHARTS="$(echo "$CHARTS" | jq -c --arg c "$EX_NAME" 'map(select(. != $c))')"
|
||||
done <<< "$(ci/run-install-test.sh --list-excluded)"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
|
||||
# An empty matrix does not fail - the install-test job would simply
|
||||
# not be created and the run would end green, having tested nothing.
|
||||
# That is the shape of signal this whole issue is about, so it is a
|
||||
# loud error instead.
|
||||
if [ "$CHARTS" = "[]" ]; then
|
||||
echo "::error::no chart left to test - every candidate is listed in ci/excluded-charts.txt."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
COUNT="$(echo "$CHARTS" | jq -r 'length')"
|
||||
echo "charts=$CHARTS" >> "$GITHUB_OUTPUT"
|
||||
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
|
||||
echo "Install-test matrix ($COUNT charts): $CHARTS"
|
||||
|
||||
# --- The run record, first half -------------------------------------
|
||||
# What was asked for, when, and against what. The per-chart outcome
|
||||
# and the environment each leg measured are written by the legs
|
||||
# themselves (the called workflow's "Record the result" step), the
|
||||
# verdict by the record job below. A green run nobody notes is
|
||||
# unknown again next time.
|
||||
{
|
||||
echo "## All-charts install-test"
|
||||
echo
|
||||
echo "- Started: **$(date -u '+%Y-%m-%d %H:%M') UTC**"
|
||||
echo "- Requested: $( [ "$SUBSET" = true ] && echo "subset \`$REQUESTED\`" || echo "**every installable chart**" )"
|
||||
echo "- Matrix: **$COUNT charts** — $(echo "$CHARTS" | jq -r 'map("`" + . + "`") | join(", ")')"
|
||||
echo "- Expected runtime, so a deviation is visible as one: **20 charts in 12m41s**, measured on Gitea PR #48 — one job, all legs concurrently on one Docker daemon. The job timeout is 30 minutes per leg."
|
||||
echo "- One throwaway k3d cluster per chart, no reuse: a reused cluster would carry leftovers, a warm image cache and fixtures already applied, and \"came up from nothing\" is the statement this test exists to make."
|
||||
echo
|
||||
echo "Each chart's outcome, with the runner label, \`K3S_IMAGE\` and \`kubectl\` version it was measured against, is in the install-test job summaries; the verdict over the whole matrix is at the end."
|
||||
echo
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
if [ -n "$SUMMARY_ROWS" ]; then
|
||||
{
|
||||
echo "### Charts excluded from the install-test"
|
||||
echo
|
||||
echo "These charts are listed in \`ci/excluded-charts.txt\` and get **no** install-test job. There is no green tick for them, because nothing was attempted (issue #231)."
|
||||
echo
|
||||
echo "| Chart | Documented reason |"
|
||||
echo "| --- | --- |"
|
||||
printf '%s' "$SUMMARY_ROWS"
|
||||
echo
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
# The same workflow validate-charts.yml calls, with the same inputs, differing
|
||||
# only in the chart list. Not a copy of the job: see the header of
|
||||
# .github/workflows/install-test.yml for what would drift.
|
||||
all-charts:
|
||||
name: "All charts"
|
||||
needs: select-charts
|
||||
uses: ./.github/workflows/install-test.yml
|
||||
with:
|
||||
charts: ${{ needs.select-charts.outputs.charts }}
|
||||
secrets: inherit
|
||||
|
||||
# The verdict over the matrix as a whole. A matrix leg only ever sees its own
|
||||
# job summary, so this is the one place that can say "all of them" - and the
|
||||
# aggregate is what the question "has the environment changed" is answered
|
||||
# with. It runs on failure too, hence "if: always()".
|
||||
record:
|
||||
name: "Record the run"
|
||||
needs: [select-charts, all-charts]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Write the verdict"
|
||||
env:
|
||||
RESULT: ${{ needs.all-charts.result }}
|
||||
COUNT: ${{ needs.select-charts.outputs.count }}
|
||||
run: |
|
||||
case "$RESULT" in
|
||||
success)
|
||||
VERDICT="**all $COUNT charts installed and became Ready** on this runner, on $(date -u '+%Y-%m-%d')"
|
||||
;;
|
||||
failure)
|
||||
VERDICT="**at least one chart did not come up.** Which ones, and whether the chart failed or the cluster never came up, is in the per-chart records above. That is a result and not an accident to re-run: rss-bridge failing this way on Gitea PR #48 is why Gitea #51 exists"
|
||||
;;
|
||||
skipped|cancelled)
|
||||
VERDICT="**no verdict** - the matrix did not run ($RESULT). Nothing was tested; do not read this run as a green one"
|
||||
;;
|
||||
*)
|
||||
VERDICT="**no verdict** - unexpected matrix result '$RESULT'"
|
||||
;;
|
||||
esac
|
||||
{
|
||||
echo "## Verdict"
|
||||
echo
|
||||
echo "$VERDICT"
|
||||
echo
|
||||
echo "Worth writing down where it belongs: a result from this run is what the next \"has anything drifted\" question is compared against. If it moved a chart forward or backward, say so on the chart's wiki page or in \`Lessons-Learned\`."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -1,414 +0,0 @@
|
||||
# The install-test, as one job with two callers.
|
||||
#
|
||||
# Both callers install a chart the SAME way; they differ only in which charts
|
||||
# they name. validate-charts.yml passes the charts a pull request changed;
|
||||
# install-test-all-charts.yml passes every installable chart on demand, to ask
|
||||
# whether the environment underneath an UNCHANGED chart still holds (Gitea
|
||||
# #52) - a chart untouched since the forge move carries a green history from a
|
||||
# different runner, and that says nothing about today.
|
||||
#
|
||||
# It is a called workflow and not a second job, for the same reason
|
||||
# ci/excluded-charts.txt has exactly one parser: a second copy is a second
|
||||
# chance to test DIFFERENTLY, unnoticed. The parts that would drift are worth
|
||||
# naming, because each was paid for once already - the pinned kubectl with its
|
||||
# skew check, envsubst, the detection of a containerised job, the per-leg
|
||||
# cluster name. A copy that slowly disagrees about any of them does not go red;
|
||||
# it turns a green run into a statement about something other than what was
|
||||
# assumed.
|
||||
#
|
||||
# What stays with the callers: WHICH charts are tested, and whether the job
|
||||
# runs at all (needs/if). Everything that decides HOW one is installed is here.
|
||||
name: Install-test
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
charts:
|
||||
required: true
|
||||
type: string
|
||||
description: 'JSON array of chart names, e.g. ["cyberchef","zipline"]. The entries of ci/excluded-charts.txt must already be gone - see the matrix comment below.'
|
||||
secrets:
|
||||
OP_CONNECT_TOKEN:
|
||||
# Needed by exactly one leg - freesailarr, for the 1Password Connect
|
||||
# lookup of its CI Wireguard key. Declared OPTIONAL on purpose, where
|
||||
# publish-helm-chart.yml declares its token required: on this instance a
|
||||
# missing secret does not fail the job, it fails job CREATION, so the
|
||||
# run shows no steps at all (README, "Where this repo lives"). With
|
||||
# required: true a pull request from a fork - which carries no secrets -
|
||||
# would lose the entire install-test that way, every chart at once, with
|
||||
# nothing naming the cause. Optional instead: only the freesailarr leg
|
||||
# fails, loudly, at the ":?" guards that name the missing op:// field.
|
||||
required: false
|
||||
description: "1Password Connect token; only the freesailarr leg reads it"
|
||||
jobs:
|
||||
install-test:
|
||||
name: "Install-test ${{ matrix.chart }}"
|
||||
# The one job in this repo that does not simply say ubuntu-latest, and the
|
||||
# expression is not decoration - the same label means different machines on
|
||||
# the two forges this repo currently lives on (#225).
|
||||
#
|
||||
# This is the only job that needs a Docker socket: it creates a k3d
|
||||
# cluster, and k3d is a Docker client. On GitHub's hosted runners every
|
||||
# label carries Docker, so the question never came up. On the Gitea
|
||||
# instance it was MEASURED on a probe pull request, per job:
|
||||
# detect-changed-charts, build-chart, the quantity render and the Trivy
|
||||
# scan all pass on `ubuntu-latest` - none of them needs Docker - while the
|
||||
# install-test dies there at "Create k3d cluster" with "Cannot connect to
|
||||
# the Docker daemon at unix:///var/run/docker.sock", an error that points
|
||||
# at k3d and never at the runner label. `ubuntu-docker` is the label that
|
||||
# carries a socket, and Gitea does evaluate this expression: the probe job
|
||||
# was picked up by the docker runner with labels ["ubuntu-docker"].
|
||||
#
|
||||
# Why an expression and not a plain `ubuntu-docker`: that label does not
|
||||
# exist on GitHub, so every chart PR would sit queued forever for as long
|
||||
# as this repo still builds on github.com - blocking the merge with no
|
||||
# error anywhere. github.server_url is the one context value that separates
|
||||
# the two without naming our instance.
|
||||
#
|
||||
# The label is necessary and was, for a while, not sufficient:
|
||||
# `ubuntu-docker`'s daemon reported "Cgroup Driver: none" plus "Running in
|
||||
# rootless-mode without cgroups", and k3d got as far as starting the k3s
|
||||
# node, which restarted immediately with an empty log because k3s needs
|
||||
# cgroup delegation (#225). Nothing in this file addressed that - it was
|
||||
# dealt with on the instance side. It no longer blocks, and the evidence is
|
||||
# Gitea PR #48: 19 of 20 matrix charts installed and became Ready on this
|
||||
# label. The twentieth, rss-bridge, failed and is Gitea #51.
|
||||
runs-on: ${{ github.server_url == 'https://github.com' && 'ubuntu-latest' || 'ubuntu-docker' }}
|
||||
# Blocking since Renovate automerges patch/minor updates: a chart that no
|
||||
# longer starts must stop the PR before it reaches main and publishes. This
|
||||
# ends the stabilization phase of issue #22 - the job reported failures
|
||||
# without blocking while it proved itself across several chart PRs.
|
||||
#
|
||||
# The 30 minutes are per leg and hold for the all-charts caller too, with
|
||||
# the measured figure to compare against: Gitea PR #48 ran 20 charts in
|
||||
# 12m41s in one job, all legs concurrently on one Docker daemon. If a run
|
||||
# ever comes near this timeout, it is raised HERE, once, for both callers -
|
||||
# a value that differs per caller would mean the two tests give up at
|
||||
# different points while claiming to be the same test.
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# The charts arrive from the caller ALREADY filtered against
|
||||
# ci/excluded-charts.txt: validate-charts.yml does it in
|
||||
# detect-changed-charts, install-test-all-charts.yml in select-charts, and
|
||||
# both go through ci/run-install-test.sh --list-excluded, the one parser
|
||||
# of that file. Both also name every chart they dropped WITH its
|
||||
# documented reason in their own job summary, because an absent matrix
|
||||
# entry says "not run" and never why (issue #231).
|
||||
#
|
||||
# DO NOT move that decision in here, e.g. as an "if" on this job
|
||||
# if: ... && !contains(fromJSON(inputs.excluded-charts), matrix.chart)
|
||||
# to get a visible "Skipped" per chart instead of an absent entry.
|
||||
# GitHub rejects it - jobs.<id>.if is evaluated BEFORE the matrix is
|
||||
# expanded, so the matrix context does not exist there:
|
||||
# context "matrix" is not allowed here.
|
||||
# available contexts are "github", "inputs", "needs", "vars".
|
||||
# Measured on run 33650724109 (PR #233, closed): the whole workflow ended
|
||||
# in failure with zero jobs created. The per-chart value has to be gone
|
||||
# from the matrix before this workflow is called.
|
||||
matrix:
|
||||
chart: ${{ fromJSON(inputs.charts) }}
|
||||
env:
|
||||
K3D_VERSION: v5.9.0
|
||||
K3S_IMAGE: docker.io/rancher/k3s:v1.35.5-k3s1
|
||||
# Pinned rather than taken from the runner image. The argument is not
|
||||
# "Gitea's image has no kubectl" - that is only how it was noticed. A
|
||||
# kubectl inherited from whatever the image happens to ship drifts
|
||||
# unannounced with every image rebuild, and an install-test is supposed
|
||||
# to run against a KNOWN client. That GitHub's hosted image already
|
||||
# carries one does not make the pin unnecessary; it only makes the drift
|
||||
# invisible there. The value mirrors K3S_IMAGE above (v1.35.5), which is
|
||||
# the cluster this client talks to.
|
||||
# renovate: datasource=github-releases depName=kubernetes/kubernetes
|
||||
KUBECTL_VERSION: v1.35.5
|
||||
# The same expression as runs-on above, written a second time because the
|
||||
# env context is not available in runs-on - so it cannot be the other way
|
||||
# round, and reading the label back off the runner is not possible either
|
||||
# (RUNNER_NAME below names the machine, not the label that picked it).
|
||||
# It exists for the run record at the end of this job: "on which runner
|
||||
# was this green tick measured" is the whole question of Gitea #52. Kept
|
||||
# right under runs-on, so a change to one that forgets the other is
|
||||
# visible on a single screen.
|
||||
RUNNER_LABEL: ${{ github.server_url == 'https://github.com' && 'ubuntu-latest' || 'ubuntu-docker' }}
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v7
|
||||
- name: "Install Helm"
|
||||
uses: azure/setup-helm@v5
|
||||
- name: "Install kubectl"
|
||||
run: |
|
||||
# kubectl and the cluster are a pair: the client talks to the server
|
||||
# K3S_IMAGE starts, and Kubernetes supports at most one minor of
|
||||
# skew between them. Renovate updates KUBECTL_VERSION on its own
|
||||
# while K3S_IMAGE is tracked by nothing, so without this check the
|
||||
# two drift apart until the client is talking to a server it no
|
||||
# longer supports - silently, because kubectl only warns. And it is
|
||||
# not theoretical: measured by dry run, a lookup from v1.34.0
|
||||
# offers v1.37.0 in ONE step, because Renovate fills the non-major
|
||||
# bucket with the newest candidate rather than walking the minors.
|
||||
# So this check WILL fire one day, on purpose, with a message that
|
||||
# says which of the two pins to move.
|
||||
#
|
||||
# Deliberately not solved with an allowedVersions constraint pinning
|
||||
# kubectl to the k3s minor: that is the nginx trap the README
|
||||
# describes - a filter that silently stops offering updates the day
|
||||
# the other side moves. A loud failure beats a quiet freeze.
|
||||
read -r KMAJ KMIN <<<"$(printf '%s' "$KUBECTL_VERSION" | sed -E 's/^v([0-9]+)\.([0-9]+).*/\1 \2/')"
|
||||
read -r SMAJ SMIN <<<"$(printf '%s' "$K3S_IMAGE" | sed -E 's/.*:v([0-9]+)\.([0-9]+).*/\1 \2/')"
|
||||
SKEW=$(( (KMAJ * 100 + KMIN) - (SMAJ * 100 + SMIN) ))
|
||||
if [ "${SKEW#-}" -gt 1 ]; then
|
||||
echo "::error::kubectl $KUBECTL_VERSION and k3s $K3S_IMAGE are more than one minor apart. Bump K3S_IMAGE to match, or pin KUBECTL_VERSION back."
|
||||
exit 1
|
||||
fi
|
||||
curl -sSfL -o /tmp/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
install -m 0755 /tmp/kubectl "$HOME/.local/bin/kubectl"
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
"$HOME/.local/bin/kubectl" version --client
|
||||
- name: "Provide envsubst"
|
||||
run: |
|
||||
# ci/run-install-test.sh renders ci/<chart>/fixtures.env.yaml with
|
||||
# envsubst - today only freesailarr has one, and without the tool
|
||||
# the fixtures collapse to "error: no objects passed to apply",
|
||||
# gluetun never gets its Wireguard key, and nothing behind its kill
|
||||
# switch becomes Ready. GitHub's hosted image ships gettext-base,
|
||||
# the Gitea runner image does not. The script is fine as it is; a
|
||||
# standard tool is simply missing.
|
||||
#
|
||||
# CHECKED FOR, NOT PINNED - and that is a different decision from
|
||||
# kubectl above, on purpose. kubectl's VERSION changes what the test
|
||||
# means: its distance to the API server is a real source of failure,
|
||||
# which is why it is pinned, tracked and guarded. envsubst
|
||||
# substitutes text; which release performs the substitution does not
|
||||
# change the result. Pinning it would buy nothing and add a line for
|
||||
# Renovate to carry. So: present - do nothing, which keeps the
|
||||
# GitHub path free of any package install; missing - fetch it, and
|
||||
# the difference between the two runners is stated rather than
|
||||
# hidden behind an unconditional apt-get.
|
||||
if command -v envsubst >/dev/null 2>&1; then
|
||||
echo "envsubst present at $(command -v envsubst) - nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
echo "envsubst missing on this runner image - installing gettext-base"
|
||||
if [ "$(id -u)" -eq 0 ]; then SUDO=""; else SUDO="sudo"; fi
|
||||
$SUDO apt-get update -qq
|
||||
$SUDO apt-get install -y -qq --no-install-recommends gettext-base
|
||||
command -v envsubst
|
||||
# freesailarr is the one chart whose install-test needs credentials:
|
||||
# without a real Wireguard key gluetun never completes a handshake, and
|
||||
# behind its kill switch not a single app container becomes Ready. The key
|
||||
# comes from 1Password Connect like the registry credentials in
|
||||
# publish-helm-chart.yml, and it is a CI-ONLY Mullvad key: Mullvad ties an
|
||||
# address to a key and sends the tunnel to the endpoint it saw last, so
|
||||
# reusing the production key would let every PR run pull the tunnel out
|
||||
# from under the running stack - with the symptom in the cluster and the
|
||||
# cause in a pull request. Loaded only for this chart; no other chart run
|
||||
# touches the VPN key.
|
||||
- name: "Configure OP-Connect"
|
||||
if: matrix.chart == 'freesailarr'
|
||||
uses: 1password/load-secrets-action/configure@v5
|
||||
with:
|
||||
connect-host: https://op-connect.jk-dev.de
|
||||
- name: "Load freesailarr VPN credentials"
|
||||
id: load-freesailarr-vpn
|
||||
if: matrix.chart == 'freesailarr'
|
||||
uses: 1password/load-secrets-action@v5
|
||||
env:
|
||||
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||
FREESAILARR_WG_KEY: op://GitHub-Actions/freesailarr-mullvad-test-config/credential
|
||||
FREESAILARR_WG_ADDRESSES: op://GitHub-Actions/freesailarr-mullvad-test-config/ip-address
|
||||
# The same trap publish-helm-chart.yml documents at length: v5 of
|
||||
# load-secrets-action does NOT export the loaded values as environment
|
||||
# variables for later steps, only its step outputs carry them, and the
|
||||
# loading step logs "Populating variable" and stays green either way. So
|
||||
# the values are mapped into each step's own env block via
|
||||
# steps.<id>.outputs.* and read as quoted shell variables. The ":?" guards
|
||||
# run before the cluster is even created and name the op:// field that is
|
||||
# missing - an empty key would otherwise reach gluetun and look like a
|
||||
# broken chart instead of a missing secret. They print the variable name,
|
||||
# never its value.
|
||||
- name: "Verify the VPN credentials arrived"
|
||||
if: matrix.chart == 'freesailarr'
|
||||
env:
|
||||
FREESAILARR_WG_KEY: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_KEY }}
|
||||
FREESAILARR_WG_ADDRESSES: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_ADDRESSES }}
|
||||
run: |-
|
||||
: "${FREESAILARR_WG_KEY:?empty - check op://GitHub-Actions/freesailarr-mullvad-test-config/credential}"
|
||||
: "${FREESAILARR_WG_ADDRESSES:?empty - check op://GitHub-Actions/freesailarr-mullvad-test-config/ip-address}"
|
||||
echo "Both VPN values arrived non-empty."
|
||||
- name: "Install k3d"
|
||||
run: curl -sSfL https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | TAG="$K3D_VERSION" bash
|
||||
# One cluster per matrix leg, named after the chart. It used to be the
|
||||
# fixed name "install-test", and that was wrong on GitHub too - it just
|
||||
# could not show there, because GitHub gives every matrix leg its own VM
|
||||
# with its own Docker daemon, so two legs never saw each other's
|
||||
# cluster. The environment hid the bug for two years rather than
|
||||
# preventing it. Gitea runs all legs of a matrix CONCURRENTLY in ONE job
|
||||
# on ONE daemon, and there the fixed name collides at once: measured on
|
||||
# the probe PR of #225, one leg logs "Re-using existing network
|
||||
# 'k3d-install-test'", the other dies on "The container name
|
||||
# /k3d-install-test-server-0 is already in use" - and then the first
|
||||
# leg's rollback deletes the shared network out from under the second,
|
||||
# which fails with "network k3d-install-test not found". Both legs red,
|
||||
# neither cause in either message. DO NOT put the fixed name back.
|
||||
#
|
||||
# Why the bare chart name and not a prefixed one: k3d rejects a cluster
|
||||
# name over 32 characters ("Cluster name must be <= 32 characters",
|
||||
# measured against k3d v5.9.0 - 32 passes, 33 fails), and every
|
||||
# container is named k3d-<cluster>-server-0 from it. The longest chart
|
||||
# here is calibre-web-automated at 21, so "install-test-<chart>" would
|
||||
# be 34 and break exactly that one chart and no other - the kind of
|
||||
# prefix that is worse than the fixed name because it only sometimes
|
||||
# breaks. The guard below states the limit instead of leaving it to be
|
||||
# rediscovered.
|
||||
- name: "Create k3d cluster"
|
||||
run: |
|
||||
CLUSTER="${{ matrix.chart }}"
|
||||
if [ "${#CLUSTER}" -gt 32 ]; then
|
||||
echo "::error::chart name '$CLUSTER' is ${#CLUSTER} characters and k3d allows at most 32 for a cluster name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Does THIS job run inside a container? That, and not which forge we
|
||||
# are on, is what decides whether k3d's cluster is reachable (#227).
|
||||
#
|
||||
# k3d publishes the API server port on the Docker *host*. When the
|
||||
# job runs straight on the host - GitHub's hosted runners - that is
|
||||
# the same machine, and the "https://0.0.0.0:<port>" k3d writes into
|
||||
# the kubeconfig works. When the job itself is a container, as every
|
||||
# Gitea Actions job is, the host is the dind container and 0.0.0.0
|
||||
# points at the job container itself, where nothing listens. The
|
||||
# cluster comes up fine and the first kubectl call is refused.
|
||||
#
|
||||
# Deliberately NOT keyed on github.server_url, although that would
|
||||
# pick the right branch today: a self-hosted GitHub runner in a
|
||||
# container has exactly this problem, and a condition that says
|
||||
# "GitHub" while meaning "not containerised" is the kind of lie this
|
||||
# repo exists to remove. So the state is detected instead of
|
||||
# inferred, and the detection is the same question the fix needs
|
||||
# answered anyway: ask the daemon about ourselves. Inside a
|
||||
# container that the daemon knows, this returns our network and k3d
|
||||
# joins it, which puts k3s and this job on one bridge where Docker's
|
||||
# own DNS resolves the container names. On a host it returns nothing,
|
||||
# because the hostname is not a container, and the old path is taken
|
||||
# unchanged.
|
||||
# The "|| true" is load-bearing: this step runs under `bash -e -o
|
||||
# pipefail`, and on a host `docker inspect` exits non-zero, which
|
||||
# would abort the step instead of taking the else branch.
|
||||
JOB_NET="$(docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$(hostname)" 2>/dev/null | awk '{print $1}' || true)"
|
||||
|
||||
if [ -n "$JOB_NET" ]; then
|
||||
echo "Containerised job: $(hostname) on docker network '$JOB_NET' - k3d joins it."
|
||||
k3d cluster create "$CLUSTER" --image "$K3S_IMAGE" --network "$JOB_NET" --wait --timeout 180s
|
||||
# Same reason: the published port is useless from in here, so the
|
||||
# kubeconfig is pointed at the load balancer container by name,
|
||||
# over the shared network. If the API server certificate did not
|
||||
# carry that name this would fail on the TLS handshake and the
|
||||
# cluster-info below would say so - it holds without an extra
|
||||
# --tls-san, measured on the probe PR of #225.
|
||||
kubectl config set-cluster "k3d-$CLUSTER" --server="https://k3d-$CLUSTER-serverlb:6443"
|
||||
else
|
||||
echo "Job runs on the docker host - k3d's published port is reachable as written."
|
||||
k3d cluster create "$CLUSTER" --image "$K3S_IMAGE" --wait --timeout 180s
|
||||
fi
|
||||
|
||||
# Reachability is proven HERE, and the shape of this check is the
|
||||
# product of getting it wrong twice. Both mistakes are written down
|
||||
# because both look like something to simplify away.
|
||||
#
|
||||
# It is a LOOP and not a single call. k3d's --wait returns when the
|
||||
# node container reports ready, but the API server can still answer
|
||||
# discovery with 503 for a moment after that. Measured: a single
|
||||
# `kubectl cluster-info` right here failed with "ServiceUnavailable:
|
||||
# the server is currently unable to handle the request" on one of
|
||||
# two legs of the SAME GitHub run while the other passed. A check
|
||||
# that is itself racy produces exactly the sporadic red this job
|
||||
# must never have (#227) - and a test people have learned to re-run
|
||||
# is one they have stopped believing.
|
||||
#
|
||||
# It counts READY NODES and not "did kubectl answer". `kubectl get
|
||||
# nodes` exits 0 on an EMPTY list, so the first version passed
|
||||
# against a cluster whose API server was up and whose node had never
|
||||
# registered: the log said "No resources found", the step went
|
||||
# green, and the failure surfaced five minutes later as a pod stuck
|
||||
# in Pending with no node and no scheduling event - nowhere near its
|
||||
# cause. A check that a broken cluster passes is worse than no
|
||||
# check. `kubectl wait --for=condition=Ready nodes --all` is not the
|
||||
# shorter way to write this: it has the identical hole, because with
|
||||
# zero nodes the condition is vacuously satisfied.
|
||||
#
|
||||
# The evidence that this version earns its keep: only after it
|
||||
# counted nodes did the real cause become visible in the dump below
|
||||
# - k3s could not start containerd ("error creating fsnotify
|
||||
# watcher: too many open files", an inotify limit on the host), and
|
||||
# nothing above this line had ever said so.
|
||||
for ATTEMPT in $(seq 1 90); do
|
||||
READY_NODES="$(kubectl get nodes --no-headers 2>/dev/null | awk '$2 == "Ready"' | wc -l | tr -d ' ')"
|
||||
[ "$READY_NODES" -ge 1 ] && break
|
||||
if [ "$ATTEMPT" -eq 90 ]; then
|
||||
echo "::error::the k3d cluster '$CLUSTER' answers but has no Ready node after 180s"
|
||||
kubectl get nodes -o wide || true
|
||||
kubectl describe nodes || true
|
||||
echo "--- k3s server log ---"
|
||||
docker logs "k3d-$CLUSTER-server-0" 2>&1 | tail -80 || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
kubectl get nodes -o wide
|
||||
# Every chart reaching this step is one the exclusion filter let through,
|
||||
# so the script's SKIP branch is never taken here - it exists for local
|
||||
# runs. The variables below are empty for every chart but the one that
|
||||
# declares them in ci/<chart>/settings.env (REQUIRED_ENV); the script
|
||||
# fails loudly if a declared one is missing, it never skips over it.
|
||||
- name: "Install chart and wait for readiness"
|
||||
# The id is read by the run record at the end of this job: its outcome
|
||||
# is what separates "the chart did not come up" from "the cluster never
|
||||
# came up", and the two mean opposite things for Gitea #52.
|
||||
id: install
|
||||
env:
|
||||
FREESAILARR_WG_KEY: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_KEY }}
|
||||
FREESAILARR_WG_ADDRESSES: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_ADDRESSES }}
|
||||
run: ci/run-install-test.sh "${{ matrix.chart }}"
|
||||
# Must carry the same per-leg name as the create step above: a blanket
|
||||
# cleanup, or the old fixed name, would tear down the neighbouring leg's
|
||||
# cluster on a runner that shares one Docker daemon - which is precisely
|
||||
# the half of the collision that turned one red leg into two.
|
||||
- name: "Delete k3d cluster"
|
||||
if: always()
|
||||
run: k3d cluster delete "${{ matrix.chart }}" || true
|
||||
# The run record (Gitea #52). A green install-test says "this chart came
|
||||
# up" and nothing about WHERE, and where is exactly what the question
|
||||
# needs: a chart that has not been touched since the forge move carries a
|
||||
# green history from another environment. So every leg writes down the
|
||||
# four facts that decide what its result means - date, runner label,
|
||||
# K3S_IMAGE, kubectl - next to its own outcome. A green run nobody notes
|
||||
# is unknown again next time.
|
||||
#
|
||||
# Per leg, and not one table for the whole matrix, because there is no
|
||||
# place to write one: a matrix leg only ever sees its own
|
||||
# GITHUB_STEP_SUMMARY, on either forge. The aggregate verdict is written
|
||||
# by the calling workflow, which does see the matrix as a whole.
|
||||
#
|
||||
# It reports steps.install.outcome and not the job status: a leg that died
|
||||
# in "Create k3d cluster" never installed anything, and recording that as
|
||||
# a failed chart would blame the chart for the environment - precisely the
|
||||
# confusion this issue is about.
|
||||
- name: "Record the result"
|
||||
if: always()
|
||||
env:
|
||||
CHART: ${{ matrix.chart }}
|
||||
INSTALL_OUTCOME: ${{ steps.install.outcome }}
|
||||
run: |
|
||||
case "$INSTALL_OUTCOME" in
|
||||
success) RESULT="installed, every workload Ready" ;;
|
||||
failure) RESULT="**FAILED** - the chart did not come up" ;;
|
||||
*) RESULT="**FAILED before the install** - cluster or tooling, not the chart (install step outcome: '${INSTALL_OUTCOME:-none}')" ;;
|
||||
esac
|
||||
{
|
||||
echo "### Install-test \`$CHART\`: $RESULT"
|
||||
echo
|
||||
echo "| Date (UTC) | Runner label | Runner name | K3S_IMAGE | kubectl |"
|
||||
echo "| --- | --- | --- | --- | --- |"
|
||||
echo "| $(date -u '+%Y-%m-%d %H:%M') | \`$RUNNER_LABEL\` | \`${RUNNER_NAME:-unknown}\` | \`$K3S_IMAGE\` | \`$KUBECTL_VERSION\` |"
|
||||
echo
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -1,15 +0,0 @@
|
||||
name: Publish Sure Helm Chart
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- sure/**
|
||||
jobs:
|
||||
publish-sure:
|
||||
name: Publish Sure Helm Chart
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'sure'
|
||||
secrets: inherit
|
||||
@@ -1,15 +0,0 @@
|
||||
name: Publish Valkey Helm Chart
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- valkey/**
|
||||
jobs:
|
||||
publish-valkey:
|
||||
name: Publish Valkey Helm Chart
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'valkey'
|
||||
secrets: inherit
|
||||
@@ -157,9 +157,8 @@ jobs:
|
||||
# against - resource quantities formatted with printf "%v", where float64
|
||||
# flips to exponential notation above ~1e6 - lives in the resources helper,
|
||||
# and that helper is copied into every chart's templates/_helpers.tpl,
|
||||
# because sharing it would mean a library chart (README, guideline 14). So
|
||||
# it comes back by copy: a new chart started from an older one, or from
|
||||
# template-chart before the fix.
|
||||
# because charts here have no dependencies. So it comes back by copy: a new
|
||||
# chart started from an older one, or from template-chart before the fix.
|
||||
# In that PR the chart is new and its copy of the bug is in no diff anyone
|
||||
# reads. Running over everything also means there is no list to maintain -
|
||||
# adding a chart enrolls it. Two renders per chart, no cluster, a few
|
||||
@@ -228,30 +227,314 @@ jobs:
|
||||
run: ci/run-trivy-config.sh "${{ matrix.chart }}"
|
||||
|
||||
install-test:
|
||||
name: "Install-test"
|
||||
name: "Install-test ${{ matrix.chart }}"
|
||||
needs: [detect-changed-charts, build-chart]
|
||||
if: needs.detect-changed-charts.outputs.any-install == 'true'
|
||||
# The job itself lives in .github/workflows/install-test.yml and is called
|
||||
# from here and from install-test-all-charts.yml (Gitea #52). Everything
|
||||
# that decides HOW a chart is installed moved there unchanged - the runner
|
||||
# label expression, the pinned kubectl with its skew check, envsubst, the
|
||||
# containerised-job detection, the per-leg cluster name, the freesailarr VPN
|
||||
# credentials. What stayed here is WHICH charts are tested and whether the
|
||||
# job runs at all. A copied job would be a second chance to test
|
||||
# differently, unnoticed, which is the same argument that gives
|
||||
# ci/excluded-charts.txt exactly one parser.
|
||||
# The one job in this repo that does not simply say ubuntu-latest, and the
|
||||
# expression is not decoration - the same label means different machines on
|
||||
# the two forges this repo currently lives on (#225).
|
||||
#
|
||||
# "install-charts", not "charts": the changed charts minus the ones in
|
||||
# ci/excluded-charts.txt, filtered in detect-changed-charts, which also
|
||||
# names every excluded chart of this pull request with its documented
|
||||
# reason in its job summary (issue #231). The called workflow's matrix
|
||||
# comment carries the rest of that lesson, including why the decision
|
||||
# cannot be made per matrix leg instead.
|
||||
uses: ./.github/workflows/install-test.yml
|
||||
with:
|
||||
charts: ${{ needs.detect-changed-charts.outputs.install-charts }}
|
||||
# As the 24 publish-<chart>.yml hulls do, and measured to work on this
|
||||
# instance (README, "Where this repo lives"). The called workflow declares
|
||||
# exactly one secret, OP_CONNECT_TOKEN, and only its freesailarr leg reads
|
||||
# it.
|
||||
secrets: inherit
|
||||
# This is the only job that needs a Docker socket: it creates a k3d
|
||||
# cluster, and k3d is a Docker client. On GitHub's hosted runners every
|
||||
# label carries Docker, so the question never came up. On the Gitea
|
||||
# instance it was MEASURED on a probe pull request, per job:
|
||||
# detect-changed-charts, build-chart, the quantity render and the Trivy
|
||||
# scan all pass on `ubuntu-latest` - none of them needs Docker - while the
|
||||
# install-test dies there at "Create k3d cluster" with "Cannot connect to
|
||||
# the Docker daemon at unix:///var/run/docker.sock", an error that points
|
||||
# at k3d and never at the runner label. `ubuntu-docker` is the label that
|
||||
# carries a socket, and Gitea does evaluate this expression: the probe job
|
||||
# was picked up by the docker runner with labels ["ubuntu-docker"].
|
||||
#
|
||||
# Why an expression and not a plain `ubuntu-docker`: that label does not
|
||||
# exist on GitHub, so every chart PR would sit queued forever for as long
|
||||
# as this repo still builds on github.com - blocking the merge with no
|
||||
# error anywhere. github.server_url is the one context value that separates
|
||||
# the two without naming our instance.
|
||||
#
|
||||
# KNOWN, and not fixed by this line: `ubuntu-docker`'s daemon is ROOTLESS
|
||||
# and reports "Cgroup Driver: none" plus "Running in rootless-mode without
|
||||
# cgroups". k3d then gets as far as starting the k3s node, which restarts
|
||||
# immediately with an empty log, because k3s needs cgroup delegation. Until
|
||||
# the instance's Docker-in-Docker provides it, this job cannot pass on
|
||||
# Gitea - see #225. The label is necessary, it is just not sufficient.
|
||||
runs-on: ${{ github.server_url == 'https://github.com' && 'ubuntu-latest' || 'ubuntu-docker' }}
|
||||
# Blocking since Renovate automerges patch/minor updates: a chart that no
|
||||
# longer starts must stop the PR before it reaches main and publishes. This
|
||||
# ends the stabilization phase of issue #22 - the job reported failures
|
||||
# without blocking while it proved itself across several chart PRs.
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# "install-charts", not "charts": the changed charts minus the ones in
|
||||
# ci/excluded-charts.txt, filtered in detect-changed-charts. Excluded
|
||||
# charts are named with their reason in that job's summary (issue #231).
|
||||
#
|
||||
# DO NOT try to move that decision into an "if" on this job, e.g.
|
||||
# if: ... && !contains(fromJSON(needs.detect-changed-charts.outputs.excluded-charts), matrix.chart)
|
||||
# to get a visible "Skipped" per chart instead of an absent entry.
|
||||
# GitHub rejects it - jobs.<id>.if is evaluated BEFORE the matrix is
|
||||
# expanded, so the matrix context does not exist there:
|
||||
# context "matrix" is not allowed here.
|
||||
# available contexts are "github", "inputs", "needs", "vars".
|
||||
# Measured on run 33650724109 (PR #233, closed): the whole workflow ended
|
||||
# in failure with zero jobs created. The per-chart value has to be gone
|
||||
# from the matrix by the time this job is reached.
|
||||
matrix:
|
||||
chart: ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }}
|
||||
env:
|
||||
K3D_VERSION: v5.9.0
|
||||
K3S_IMAGE: docker.io/rancher/k3s:v1.35.5-k3s1
|
||||
# Pinned rather than taken from the runner image. The argument is not
|
||||
# "Gitea's image has no kubectl" - that is only how it was noticed. A
|
||||
# kubectl inherited from whatever the image happens to ship drifts
|
||||
# unannounced with every image rebuild, and an install-test is supposed
|
||||
# to run against a KNOWN client. That GitHub's hosted image already
|
||||
# carries one does not make the pin unnecessary; it only makes the drift
|
||||
# invisible there. The value mirrors K3S_IMAGE above (v1.35.5), which is
|
||||
# the cluster this client talks to.
|
||||
# renovate: datasource=github-releases depName=kubernetes/kubernetes
|
||||
KUBECTL_VERSION: v1.35.5
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v7
|
||||
- name: "Install Helm"
|
||||
uses: azure/setup-helm@v5
|
||||
- name: "Install kubectl"
|
||||
run: |
|
||||
# kubectl and the cluster are a pair: the client talks to the server
|
||||
# K3S_IMAGE starts, and Kubernetes supports at most one minor of
|
||||
# skew between them. Renovate updates KUBECTL_VERSION on its own
|
||||
# while K3S_IMAGE is tracked by nothing, so without this check the
|
||||
# two drift apart until the client is talking to a server it no
|
||||
# longer supports - silently, because kubectl only warns. And it is
|
||||
# not theoretical: measured by dry run, a lookup from v1.34.0
|
||||
# offers v1.37.0 in ONE step, because Renovate fills the non-major
|
||||
# bucket with the newest candidate rather than walking the minors.
|
||||
# So this check WILL fire one day, on purpose, with a message that
|
||||
# says which of the two pins to move.
|
||||
#
|
||||
# Deliberately not solved with an allowedVersions constraint pinning
|
||||
# kubectl to the k3s minor: that is the nginx trap the README
|
||||
# describes - a filter that silently stops offering updates the day
|
||||
# the other side moves. A loud failure beats a quiet freeze.
|
||||
read -r KMAJ KMIN <<<"$(printf '%s' "$KUBECTL_VERSION" | sed -E 's/^v([0-9]+)\.([0-9]+).*/\1 \2/')"
|
||||
read -r SMAJ SMIN <<<"$(printf '%s' "$K3S_IMAGE" | sed -E 's/.*:v([0-9]+)\.([0-9]+).*/\1 \2/')"
|
||||
SKEW=$(( (KMAJ * 100 + KMIN) - (SMAJ * 100 + SMIN) ))
|
||||
if [ "${SKEW#-}" -gt 1 ]; then
|
||||
echo "::error::kubectl $KUBECTL_VERSION and k3s $K3S_IMAGE are more than one minor apart. Bump K3S_IMAGE to match, or pin KUBECTL_VERSION back."
|
||||
exit 1
|
||||
fi
|
||||
curl -sSfL -o /tmp/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
install -m 0755 /tmp/kubectl "$HOME/.local/bin/kubectl"
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
"$HOME/.local/bin/kubectl" version --client
|
||||
- name: "Provide envsubst"
|
||||
run: |
|
||||
# ci/run-install-test.sh renders ci/<chart>/fixtures.env.yaml with
|
||||
# envsubst - today only freesailarr has one, and without the tool
|
||||
# the fixtures collapse to "error: no objects passed to apply",
|
||||
# gluetun never gets its Wireguard key, and nothing behind its kill
|
||||
# switch becomes Ready. GitHub's hosted image ships gettext-base,
|
||||
# the Gitea runner image does not. The script is fine as it is; a
|
||||
# standard tool is simply missing.
|
||||
#
|
||||
# CHECKED FOR, NOT PINNED - and that is a different decision from
|
||||
# kubectl above, on purpose. kubectl's VERSION changes what the test
|
||||
# means: its distance to the API server is a real source of failure,
|
||||
# which is why it is pinned, tracked and guarded. envsubst
|
||||
# substitutes text; which release performs the substitution does not
|
||||
# change the result. Pinning it would buy nothing and add a line for
|
||||
# Renovate to carry. So: present - do nothing, which keeps the
|
||||
# GitHub path free of any package install; missing - fetch it, and
|
||||
# the difference between the two runners is stated rather than
|
||||
# hidden behind an unconditional apt-get.
|
||||
if command -v envsubst >/dev/null 2>&1; then
|
||||
echo "envsubst present at $(command -v envsubst) - nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
echo "envsubst missing on this runner image - installing gettext-base"
|
||||
if [ "$(id -u)" -eq 0 ]; then SUDO=""; else SUDO="sudo"; fi
|
||||
$SUDO apt-get update -qq
|
||||
$SUDO apt-get install -y -qq --no-install-recommends gettext-base
|
||||
command -v envsubst
|
||||
# freesailarr is the one chart whose install-test needs credentials:
|
||||
# without a real Wireguard key gluetun never completes a handshake, and
|
||||
# behind its kill switch not a single app container becomes Ready. The key
|
||||
# comes from 1Password Connect like the registry credentials in
|
||||
# publish-helm-chart.yml, and it is a CI-ONLY Mullvad key: Mullvad ties an
|
||||
# address to a key and sends the tunnel to the endpoint it saw last, so
|
||||
# reusing the production key would let every PR run pull the tunnel out
|
||||
# from under the running stack - with the symptom in the cluster and the
|
||||
# cause in a pull request. Loaded only for this chart; no other chart run
|
||||
# touches the VPN key.
|
||||
- name: "Configure OP-Connect"
|
||||
if: matrix.chart == 'freesailarr'
|
||||
uses: 1password/load-secrets-action/configure@v5
|
||||
with:
|
||||
connect-host: https://op-connect.jk-dev.de
|
||||
- name: "Load freesailarr VPN credentials"
|
||||
id: load-freesailarr-vpn
|
||||
if: matrix.chart == 'freesailarr'
|
||||
uses: 1password/load-secrets-action@v5
|
||||
env:
|
||||
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||
FREESAILARR_WG_KEY: op://GitHub-Actions/freesailarr-mullvad-test-config/credential
|
||||
FREESAILARR_WG_ADDRESSES: op://GitHub-Actions/freesailarr-mullvad-test-config/ip-address
|
||||
# The same trap publish-helm-chart.yml documents at length: v5 of
|
||||
# load-secrets-action does NOT export the loaded values as environment
|
||||
# variables for later steps, only its step outputs carry them, and the
|
||||
# loading step logs "Populating variable" and stays green either way. So
|
||||
# the values are mapped into each step's own env block via
|
||||
# steps.<id>.outputs.* and read as quoted shell variables. The ":?" guards
|
||||
# run before the cluster is even created and name the op:// field that is
|
||||
# missing - an empty key would otherwise reach gluetun and look like a
|
||||
# broken chart instead of a missing secret. They print the variable name,
|
||||
# never its value.
|
||||
- name: "Verify the VPN credentials arrived"
|
||||
if: matrix.chart == 'freesailarr'
|
||||
env:
|
||||
FREESAILARR_WG_KEY: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_KEY }}
|
||||
FREESAILARR_WG_ADDRESSES: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_ADDRESSES }}
|
||||
run: |-
|
||||
: "${FREESAILARR_WG_KEY:?empty - check op://GitHub-Actions/freesailarr-mullvad-test-config/credential}"
|
||||
: "${FREESAILARR_WG_ADDRESSES:?empty - check op://GitHub-Actions/freesailarr-mullvad-test-config/ip-address}"
|
||||
echo "Both VPN values arrived non-empty."
|
||||
- name: "Install k3d"
|
||||
run: curl -sSfL https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | TAG="$K3D_VERSION" bash
|
||||
# One cluster per matrix leg, named after the chart. It used to be the
|
||||
# fixed name "install-test", and that was wrong on GitHub too - it just
|
||||
# could not show there, because GitHub gives every matrix leg its own VM
|
||||
# with its own Docker daemon, so two legs never saw each other's
|
||||
# cluster. The environment hid the bug for two years rather than
|
||||
# preventing it. Gitea runs all legs of a matrix CONCURRENTLY in ONE job
|
||||
# on ONE daemon, and there the fixed name collides at once: measured on
|
||||
# the probe PR of #225, one leg logs "Re-using existing network
|
||||
# 'k3d-install-test'", the other dies on "The container name
|
||||
# /k3d-install-test-server-0 is already in use" - and then the first
|
||||
# leg's rollback deletes the shared network out from under the second,
|
||||
# which fails with "network k3d-install-test not found". Both legs red,
|
||||
# neither cause in either message. DO NOT put the fixed name back.
|
||||
#
|
||||
# Why the bare chart name and not a prefixed one: k3d rejects a cluster
|
||||
# name over 32 characters ("Cluster name must be <= 32 characters",
|
||||
# measured against k3d v5.9.0 - 32 passes, 33 fails), and every
|
||||
# container is named k3d-<cluster>-server-0 from it. The longest chart
|
||||
# here is calibre-web-automated at 21, so "install-test-<chart>" would
|
||||
# be 34 and break exactly that one chart and no other - the kind of
|
||||
# prefix that is worse than the fixed name because it only sometimes
|
||||
# breaks. The guard below states the limit instead of leaving it to be
|
||||
# rediscovered.
|
||||
- name: "Create k3d cluster"
|
||||
run: |
|
||||
CLUSTER="${{ matrix.chart }}"
|
||||
if [ "${#CLUSTER}" -gt 32 ]; then
|
||||
echo "::error::chart name '$CLUSTER' is ${#CLUSTER} characters and k3d allows at most 32 for a cluster name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Does THIS job run inside a container? That, and not which forge we
|
||||
# are on, is what decides whether k3d's cluster is reachable (#227).
|
||||
#
|
||||
# k3d publishes the API server port on the Docker *host*. When the
|
||||
# job runs straight on the host - GitHub's hosted runners - that is
|
||||
# the same machine, and the "https://0.0.0.0:<port>" k3d writes into
|
||||
# the kubeconfig works. When the job itself is a container, as every
|
||||
# Gitea Actions job is, the host is the dind container and 0.0.0.0
|
||||
# points at the job container itself, where nothing listens. The
|
||||
# cluster comes up fine and the first kubectl call is refused.
|
||||
#
|
||||
# Deliberately NOT keyed on github.server_url, although that would
|
||||
# pick the right branch today: a self-hosted GitHub runner in a
|
||||
# container has exactly this problem, and a condition that says
|
||||
# "GitHub" while meaning "not containerised" is the kind of lie this
|
||||
# repo exists to remove. So the state is detected instead of
|
||||
# inferred, and the detection is the same question the fix needs
|
||||
# answered anyway: ask the daemon about ourselves. Inside a
|
||||
# container that the daemon knows, this returns our network and k3d
|
||||
# joins it, which puts k3s and this job on one bridge where Docker's
|
||||
# own DNS resolves the container names. On a host it returns nothing,
|
||||
# because the hostname is not a container, and the old path is taken
|
||||
# unchanged.
|
||||
# The "|| true" is load-bearing: this step runs under `bash -e -o
|
||||
# pipefail`, and on a host `docker inspect` exits non-zero, which
|
||||
# would abort the step instead of taking the else branch.
|
||||
JOB_NET="$(docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$(hostname)" 2>/dev/null | awk '{print $1}' || true)"
|
||||
|
||||
if [ -n "$JOB_NET" ]; then
|
||||
echo "Containerised job: $(hostname) on docker network '$JOB_NET' - k3d joins it."
|
||||
k3d cluster create "$CLUSTER" --image "$K3S_IMAGE" --network "$JOB_NET" --wait --timeout 180s
|
||||
# Same reason: the published port is useless from in here, so the
|
||||
# kubeconfig is pointed at the load balancer container by name,
|
||||
# over the shared network. If the API server certificate did not
|
||||
# carry that name this would fail on the TLS handshake and the
|
||||
# cluster-info below would say so - it holds without an extra
|
||||
# --tls-san, measured on the probe PR of #225.
|
||||
kubectl config set-cluster "k3d-$CLUSTER" --server="https://k3d-$CLUSTER-serverlb:6443"
|
||||
else
|
||||
echo "Job runs on the docker host - k3d's published port is reachable as written."
|
||||
k3d cluster create "$CLUSTER" --image "$K3S_IMAGE" --wait --timeout 180s
|
||||
fi
|
||||
|
||||
# Reachability is proven HERE, and the shape of this check is the
|
||||
# product of getting it wrong twice. Both mistakes are written down
|
||||
# because both look like something to simplify away.
|
||||
#
|
||||
# It is a LOOP and not a single call. k3d's --wait returns when the
|
||||
# node container reports ready, but the API server can still answer
|
||||
# discovery with 503 for a moment after that. Measured: a single
|
||||
# `kubectl cluster-info` right here failed with "ServiceUnavailable:
|
||||
# the server is currently unable to handle the request" on one of
|
||||
# two legs of the SAME GitHub run while the other passed. A check
|
||||
# that is itself racy produces exactly the sporadic red this job
|
||||
# must never have (#227) - and a test people have learned to re-run
|
||||
# is one they have stopped believing.
|
||||
#
|
||||
# It counts READY NODES and not "did kubectl answer". `kubectl get
|
||||
# nodes` exits 0 on an EMPTY list, so the first version passed
|
||||
# against a cluster whose API server was up and whose node had never
|
||||
# registered: the log said "No resources found", the step went
|
||||
# green, and the failure surfaced five minutes later as a pod stuck
|
||||
# in Pending with no node and no scheduling event - nowhere near its
|
||||
# cause. A check that a broken cluster passes is worse than no
|
||||
# check. `kubectl wait --for=condition=Ready nodes --all` is not the
|
||||
# shorter way to write this: it has the identical hole, because with
|
||||
# zero nodes the condition is vacuously satisfied.
|
||||
#
|
||||
# The evidence that this version earns its keep: only after it
|
||||
# counted nodes did the real cause become visible in the dump below
|
||||
# - k3s could not start containerd ("error creating fsnotify
|
||||
# watcher: too many open files", an inotify limit on the host), and
|
||||
# nothing above this line had ever said so.
|
||||
for ATTEMPT in $(seq 1 90); do
|
||||
READY_NODES="$(kubectl get nodes --no-headers 2>/dev/null | awk '$2 == "Ready"' | wc -l | tr -d ' ')"
|
||||
[ "$READY_NODES" -ge 1 ] && break
|
||||
if [ "$ATTEMPT" -eq 90 ]; then
|
||||
echo "::error::the k3d cluster '$CLUSTER' answers but has no Ready node after 180s"
|
||||
kubectl get nodes -o wide || true
|
||||
kubectl describe nodes || true
|
||||
echo "--- k3s server log ---"
|
||||
docker logs "k3d-$CLUSTER-server-0" 2>&1 | tail -80 || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
kubectl get nodes -o wide
|
||||
# Every chart reaching this step is one the exclusion filter let through,
|
||||
# so the script's SKIP branch is never taken here - it exists for local
|
||||
# runs. The variables below are empty for every chart but the one that
|
||||
# declares them in ci/<chart>/settings.env (REQUIRED_ENV); the script
|
||||
# fails loudly if a declared one is missing, it never skips over it.
|
||||
- name: "Install chart and wait for readiness"
|
||||
env:
|
||||
FREESAILARR_WG_KEY: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_KEY }}
|
||||
FREESAILARR_WG_ADDRESSES: ${{ steps.load-freesailarr-vpn.outputs.FREESAILARR_WG_ADDRESSES }}
|
||||
run: ci/run-install-test.sh "${{ matrix.chart }}"
|
||||
# Must carry the same per-leg name as the create step above: a blanket
|
||||
# cleanup, or the old fixed name, would tear down the neighbouring leg's
|
||||
# cluster on a runner that shares one Docker daemon - which is precisely
|
||||
# the half of the collision that turned one red leg into two.
|
||||
- name: "Delete k3d cluster"
|
||||
if: always()
|
||||
run: k3d cluster delete "${{ matrix.chart }}" || true
|
||||
@@ -30,9 +30,7 @@ git clone ssh://git@git.jk-dev.de:30022/jlab-cluster/helm-charts.wiki.git
|
||||
|
||||
Flat layout, one Markdown file per page, page name = file name, links are relative page names without the extension (`[CI/CD](CI-CD)`). A new page must also be listed in `_Sidebar.md`, or it is reachable only by search.
|
||||
|
||||
**There is no PR flow here, so fetch and rebase before every push and never force-push** — two agents have already edited the same page minutes apart, and a force push deletes the other edit silently, with nothing left to report it.
|
||||
|
||||
**What obliges a wiki edit** (step 5 of the workflow above, same round of work, never a follow-up issue):
|
||||
**What obliges a wiki edit** (step 7 of the workflow above, same round of work, never a follow-up issue):
|
||||
|
||||
| Change | Page |
|
||||
| --- | --- |
|
||||
@@ -45,7 +43,7 @@ Flat layout, one Markdown file per page, page name = file name, links are relati
|
||||
| a Renovate rule added or changed | `Renovate` |
|
||||
| something broke in a way a rule now prevents | `Lessons-Learned` |
|
||||
|
||||
**What does not oblige an edit:** a plain app-version bump. Renovate automerges those continuously and never touches the wiki, which is why chart pages carry no version numbers at all and name `Chart.yaml` as the source of truth. An app update that *does* change values or behaviour is not a plain bump, and the table applies.
|
||||
**What does not oblige an edit:** a plain app-version bump. Renovate automerges those continuously and never touches the wiki, which is why chart pages quote versions as snapshots and name `Chart.yaml` as the source of truth. An app update that *does* change values or behaviour is not a plain bump, and the table applies.
|
||||
|
||||
Wiki commits follow the same rules as repo commits: no `Co-Authored-By`, no "Generated with Claude Code" footer.
|
||||
|
||||
@@ -54,9 +52,9 @@ Wiki commits follow the same rules as repo commits: no `Co-Authored-By`, no "Gen
|
||||
Everything runs on the Gitea instance JLab-Git: issues, pull requests, CI and Renovate. github.com is a push mirror with Actions and Renovate **switched off**, and it lags until the mirror runs. README, "Where this repo lives", holds the measured differences between a Gitea runner and a GitHub one.
|
||||
|
||||
- **`tea`, not `gh`.** Work happens in a **Coder workspace, and it has no `op`** — that is the normal case here, not a broken environment, so do not wait for the 1Password CLI or propose installing it. Call `tea` directly; it carries the workspace's stored login and opens PRs and issues under the **bot account**, which is accepted. Say so in the report when something is authored that way. (`op plugin run -- tea`, the older instruction, applies only where `op` actually exists.)
|
||||
- **The remote is `origin`, and it points at Gitea** (`ssh://git@git.jk-dev.de:30022/jlab-cluster/helm-charts.git`). The checkout has exactly that one, and so does every worktree cut from it, so a bare `git push` goes to the leading copy; the github.com mirror is fed by Gitea itself and is never pushed to by hand. A clone elsewhere may carry the mirror as a second remote named `github` — wherever more than one exists, say which one you mean.
|
||||
- **Remotes are named `github` and `JLab-Git`** — there is no `origin`. `JLab-Git/main` is the leading copy; say which remote you mean, because `git push` without one is ambiguous here.
|
||||
- **Branch protection with a signature requirement is active**, and signatures verify on Gitea. A commit that is not signed does not reach `main` — check before opening a PR, do not discover it at merge time.
|
||||
- **No merge queue.** Gitea has none, and its place is taken by **auto-merge on green checks**: the PR is armed once and merges itself when the required check passes **and the required review exists** — both are active on `main`, so an armed PR without a review sits there instead of merging. Rule 7 above is unchanged — the user merges, and an agent never merges its own PR or arms auto-merge on it.
|
||||
- **No merge queue.** Gitea has none, and its place is taken by **auto-merge on green checks**: the PR is armed once and merges itself when the required check passes **and the required review exists** — both are active on `main`, so an armed PR without a review sits there instead of merging. Rule 6 above is unchanged — the user merges, and an agent never merges its own PR or arms auto-merge on it.
|
||||
- **A required check per chart is not possible.** Gitea runs a whole matrix as one job under one status context, so the install-test reports once for all charts. What is required on `main` is `validate-charts`, as that single context; per chart is the part that cannot be had. Relevant whenever branch protection is touched.
|
||||
- **Issue numbers changed with the move, and `#n` in this repo's files means the GitHub tracker.** The issues open at the time are Gitea #5–#21 and #24; the mapping is a comment on Gitea #14. Closed ones were not migrated and exist only on GitHub. Beware that Gitea silently resolves a bare `#n` against its own counter, so an old reference can render as a link to something unrelated. When you write a *new* reference, use the Gitea number.
|
||||
- **Renovate runs outside this repo**, as the shared `jlab-renovate-bot` instance; it reads our `renovate.json5` and its `postUpgradeTasks` hook is allow-listed there. **Do not add a Renovate workflow here** — a second runner means duplicate PRs and two schedulers rewriting each other's branches. README, "Renovate on Gitea", has the evidence.
|
||||
|
||||
@@ -32,11 +32,9 @@ Every chart has a wiki page of its own — `https://git.jk-dev.de/jlab-cluster/h
|
||||
| `samba` | Samba | StatefulSet, users secret + config ConfigMap |
|
||||
| `satisfactory` | Satisfactory game server | StatefulSet, no ingress |
|
||||
| `searxng` | SearXNG | Stateless Deployment + settings ConfigMap (`use_default_settings: true` plus a free-form `settings.extra` passthrough); the signing key is a required `OnePasswordItem`, an external Valkey an optional second one. `ingress.domain` is read twice — Ingress host and `SEARXNG_BASE_URL` — so it stays required with the Ingress switched off |
|
||||
| `sure` | Sure (we-promise, the maintained Maybe-finance fork) | One StatefulSet with **two containers**: the Puma web process and a Sidekiq worker, sharing one ReadWriteOnce PVC at `/rails/storage` — that shared volume is the reason they live in one pod. Only the web container migrates (the image entrypoint runs `db:prepare` solely for the `./bin/rails server` command, so the worker is started through `args:` and skips it). Three 1Password items (app/database/redis); Postgres and Redis stay external. Readiness deliberately probes `/` rather than Rails' `/up`, which is a boot check and touches no database; the worker's three probes ask `sidekiqmon` for this pod's own heartbeat. `ingress.domain` is read for `APP_DOMAIN` and the WebAuthn relying party, so it stays required with the Ingress switched off |
|
||||
| `teamspeak-server` | TeamSpeak server | StatefulSet, no ingress |
|
||||
| `template-chart` | — | Scaffold for new charts (placeholder `xxx`), never published |
|
||||
| `urlaubsverwaltung` | Urlaubsverwaltung | Deployment, DB + OIDC secrets |
|
||||
| `valkey` | Valkey | One instance per application (cache or queue backend), replacing the shared `cluster-redis` whose single ACL file coupled six applications together. StatefulSet with one replica + ClusterIP Service, no cluster mode, no Ingress and no value to add one. The password arrives as a **finished configuration line** (`requirepass <secret>`) in the 1Password field `auth-conf`, is mounted as a file and `include`d by the config — never in argv, never in the environment; an init container refuses to start the pod unless that line is really there, because the alternative failure is an open server that looks healthy. `maxmemory` is **derived from the computed memory limit** and a configured value above it aborts the render. Persistence and eviction are a pair, not two switches (queue = PVC + `noeviction`, the default; cache = emptyDir + `allkeys-lru`). First chart carrying a **NetworkPolicy** (on by default, Gitea #5), and the first whose probes had to be proven capable of failing: `valkey-cli` exits 0 on an error reply unless `-e` is given |
|
||||
| `voucher-vault` | VoucherVault | StatefulSet, DB + OIDC secrets |
|
||||
| `ytdlp-web-player` | ytdlp_web_player | Stateless Deployment, video cache and tool cache as emptyDirs (no PVC). **The only chart with a dependency**: an optional `oauth2-proxy` subchart, off by default, wired in front of the app because the application ships no authentication at all — see "Chart dependencies" below |
|
||||
| `zipline` | Zipline | StatefulSet, DB secret |
|
||||
@@ -59,19 +57,17 @@ Every chart has a wiki page of its own — `https://git.jk-dev.de/jlab-cluster/h
|
||||
Note: the registry **accepts a re-upload of an already published version** (HTTP 201) and replaces the stored artifact, so a republish of an existing version is still silent — see #160.
|
||||
- `change-app-version.yml` — manually dispatched app-version bump; the chart must be listed in its `chart-name` choice list.
|
||||
- `ci/helm-dependencies.sh <chart-dir>` — not a workflow but the step they all share: fetches a chart's declared dependencies into `charts/`, and returns silently for the charts that declare none (all but one). It runs in **five** places, because every helm command this repo builds on exits 1 while a declared dependency is missing: before lint and package in `validate-charts.yml`, before package in `publish-helm-chart.yml`, and inside `ci/run-install-test.sh`, `ci/run-quantity-render-test.sh` and `ci/run-trivy-config.sh`. See "Chart dependencies" in the style guidelines for what that costs.
|
||||
- `validate-charts.yml` — PR pipeline: detects which charts a PR touches and, for exactly those, runs `helm lint --strict`, `helm package`, and a chart-version bump check (`template-chart` is exempt from the version check). PRs without chart changes skip the build matrix. After the build stage, an **install-test** job (the same changed-charts matrix, minus the charts listed in `ci/excluded-charts.txt`) installs each changed chart into an ephemeral [k3d](https://k3d.io) cluster on the runner and requires every workload to become Ready — see below. Both matrices follow the changed chart directories, so a PR that only changes the harness under `ci/` exercises nothing by itself; the Trivy scan has a fixed-sample fallback for that case, the install-test has none — bump the affected chart's patch version to pull it into the matrix, or run the all-charts install-test after the merge. The **resource-quantity render test** is the exception to that rule: it runs over every chart on every PR — see below.
|
||||
- `install-test.yml` — reusable workflow (`workflow_call`) holding the install-test **job** itself: the runner-label expression, the pinned `kubectl` with its skew check, `envsubst`, the containerised-job detection, the per-leg k3d cluster and the `freesailarr` VPN credentials. Its one input is a JSON array of chart names; its callers decide *which* charts are tested and whether the job runs at all, never *how*. Two callers: `validate-charts.yml` and `install-test-all-charts.yml`. **It is not copied for the second caller** — a second copy is a second chance to test differently, unnoticed, the same argument that gives `ci/excluded-charts.txt` exactly one parser.
|
||||
- `install-test-all-charts.yml` — manually dispatched **all-charts install-test**: the same job, with a matrix over every installable chart instead of the changed ones. Optional input for a subset of charts (empty = all). See below for when to run it.
|
||||
- `validate-charts.yml` — PR pipeline: detects which charts a PR touches and, for exactly those, runs `helm lint --strict`, `helm package`, and a chart-version bump check (`template-chart` is exempt from the version check). PRs without chart changes skip the build matrix. After the build stage, an **install-test** job (the same changed-charts matrix, minus the charts listed in `ci/excluded-charts.txt`) installs each changed chart into an ephemeral [k3d](https://k3d.io) cluster on the runner and requires every workload to become Ready — see below. Both matrices follow the changed chart directories, so a PR that only changes the harness under `ci/` exercises nothing by itself; the Trivy scan has a fixed-sample fallback for that case, the install-test has none — bump the affected chart's patch version to pull it into the matrix. The **resource-quantity render test** is the exception to that rule: it runs over every chart on every PR — see below.
|
||||
|
||||
### Where this repo lives, and what a Gitea runner does differently
|
||||
|
||||
Everything below was measured on this instance, not taken from documentation, and it is the reason for every deviation in `validate-charts.yml`. How the move got here is not repeated — that is Gitea #14.
|
||||
|
||||
**Reusable workflows with `secrets: inherit` work**, so the 24 thin `publish-<chart>.yml` hulls run unchanged. One quirk worth knowing before debugging the wrong thing: a *missing* secret does not fail the job, it fails job **creation**, so the run shows no steps at all.
|
||||
**Reusable workflows with `secrets: inherit` work**, so the 23 thin `publish-<chart>.yml` hulls run unchanged. One quirk worth knowing before debugging the wrong thing: a *missing* secret does not fail the job, it fails job **creation**, so the run shows no steps at all.
|
||||
|
||||
**Runner labels mean different machines than on GitHub.** `ubuntu-latest` here has **no Docker socket**; `ubuntu-docker` has one. Exactly one job cares — the install-test, because k3d is a Docker client — and its `runs-on` is therefore an expression rather than a fixed label, so the same file still works on a GitHub runner where `ubuntu-docker` does not exist. Every other job (lint/package, the quantity render, the Trivy scan) needs no socket and stays on `ubuntu-latest`.
|
||||
|
||||
**A matrix runs concurrently inside ONE job, under ONE status context.** This is the single most consequential difference and it has two halves. Operationally: all legs share one Docker daemon, which is what makes a per-leg resource name mandatory (see the cluster name below). For branch protection: **a required check per chart is not possible**, because there is only one context for the whole matrix — and its name carries the *unexpanded* matrix expression, e.g. `Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request)`. Required-check names have to be read off a real run; they cannot be copied from GitHub. That example name is from before the install-test job moved into the reusable `install-test.yml` (Gitea #52) — the matrix expression now reads `${{ fromJSON(inputs.charts) }}` and the context may carry the calling job's name as well, so **read the current name off the next chart PR before using it anywhere**. What *is* required on `main` today is **`validate-charts`**, as the single context it reports under, alongside a **review**: per chart is the part that is impossible, not required checks as such. Both are what makes Renovate's `platformAutomerge: true` safe — see "PR policy".
|
||||
**A matrix runs concurrently inside ONE job, under ONE status context.** This is the single most consequential difference and it has two halves. Operationally: all legs share one Docker daemon, which is what makes a per-leg resource name mandatory (see the cluster name below). For branch protection: **a required check per chart is not possible**, because there is only one context for the whole matrix — and its name carries the *unexpanded* matrix expression, e.g. `Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request)`. Required-check names have to be read off a real run; they cannot be copied from GitHub. What *is* required on `main` today is **`validate-charts`**, as the single context it reports under, alongside a **review**: per chart is the part that is impossible, not required checks as such. Both are what makes Renovate's `platformAutomerge: true` safe — see "PR policy".
|
||||
|
||||
**Expected runtimes**, so that a later deviation is visible as one:
|
||||
|
||||
@@ -80,7 +76,6 @@ Everything below was measured on this instance, not taken from documentation, an
|
||||
| `Create k3d cluster` | 19–25 s | 22–23 s |
|
||||
| `install-test` job, two charts in the matrix (cyberchef + freesailarr) | 210–216 s over three runs, spread 6 s | ~145 s wall, two parallel VMs |
|
||||
| `install-test` job, freesailarr alone, ten containers and a real tunnel | 225 s (measured on the MediathekArr change, Gitea #20) | not comparable — Actions there were already off |
|
||||
| `install-test` job, **20 charts** in the matrix — the accidental all-charts run of Gitea PR #48 | **12m41s**, one job, all legs concurrently on one Docker daemon; 19 of 20 charts Ready, the twentieth is Gitea #51 | never ran there |
|
||||
|
||||
The runner is **not slower** — cluster creation is a touch faster. The difference in total time is **structural, not performance**: two matrix legs share one runner here, while GitHub gives each leg its own VM. Reading it as "Gitea is slower" leads to the wrong conclusion.
|
||||
|
||||
@@ -133,8 +128,6 @@ Two details worth knowing before touching it:
|
||||
|
||||
For every chart a PR changes, `ci/run-install-test.sh <chart>` runs against a throwaway k3d cluster: it applies the stub `OnePasswordItem` CRD plus the chart's fixtures, `helm install`s the chart with its CI values, and waits for all Deployments/StatefulSets to roll out (per-chart timeout). Since probes and security hardening are active on all workloads, "Ready" means the app actually answers. On failure the job dumps pod descriptions, events and container logs.
|
||||
|
||||
The job itself is the reusable workflow **`install-test.yml`**; `validate-charts.yml` calls it with the changed charts, `install-test-all-charts.yml` with all of them. Everything about *how* a chart is installed is in that one file, deliberately — see "All-charts install-test" below.
|
||||
|
||||
Fixtures live at the **repo root** under `ci/` (deliberately *not* inside the chart directories, so they are neither packaged into the chart tgz nor trigger the publish workflows on merge):
|
||||
|
||||
- `ci/common/` — fixtures applied for every chart (schema-less stub CRD for `OnePasswordItem`)
|
||||
@@ -150,20 +143,6 @@ Fixtures live at the **repo root** under `ci/` (deliberately *not* inside the ch
|
||||
|
||||
Fixture images avoid Docker Hub where possible (`public.ecr.aws`/`ghcr.io`) to dodge runner rate limits. The job **blocks the PR** on failure — the stabilization phase of #22 ended when Renovate started automerging patch/minor updates, since a chart that no longer starts must not reach `main` and publish unattended. When adding a new chart, add its `ci/<chart>/` fixtures (or an entry in `ci/excluded-charts.txt`) — the install-test fails if both are missing. The script also runs locally against any existing cluster: `k3d cluster create t && ci/run-install-test.sh <chart> && k3d cluster delete t`.
|
||||
|
||||
### All-charts install-test (on demand)
|
||||
|
||||
`install-test-all-charts.yml` runs **the same job** as the PR pipeline — the same `install-test.yml`, the same pinned `kubectl`, the same fixtures — with a matrix over **every installable chart** instead of the charts a PR changed. `workflow_dispatch` only, **no schedule**: a daily run would spend compute on a question that changes rarely, and a red run nobody asked for is a red run nobody reads.
|
||||
|
||||
It answers a different question from the PR pipeline. The PR install-test guards against what a change breaks; this one asks whether the environment under an **unchanged** chart still holds. The install-test matrix follows the charts a PR touches, so a chart nobody has edited since the forge move carries a green history from a *different runner* — and that green says nothing about today. **Run it after a change to the runner, to `K3S_IMAGE`, to the pinned `KUBECTL_VERSION`, to the fixtures under `ci/` — and after a forge move.** It is also the answer for a PR that only changes the harness and therefore tests nothing by itself: run it once the change is on `main`.
|
||||
|
||||
Why it exists at all: the answer arrived once by accident. Gitea PR #48 corrected a *comment* in 21 charts, which made all of them "changed" and turned the run into the first all-charts install-test after the move — **19 of 20 matrix charts came up in 12m41s** in one job, and exactly one did not (`rss-bridge`, whose last green run was from a GitHub runner; Gitea #51). Worth having, and nobody had planned it.
|
||||
|
||||
- **Input `charts`** — space- or comma-separated chart names, empty means all. That is what lets a single chart be tested against the runner, which is what Gitea #51 needs. A name that is not a chart directory, or one listed in `ci/excluded-charts.txt`, is a **loud error**: someone typed it, and a green run over zero charts would be the worst possible answer. An empty matrix fails the run for the same reason.
|
||||
- **One throwaway cluster per chart, no reuse.** Reuse would save time and change what the test proves — leftovers from the previous install, a warm image cache, fixtures already applied. "Came up from nothing" is the statement.
|
||||
- **Exclusions come from the one parser**, `ci/run-install-test.sh --list-excluded`, and the excluded charts appear in the job summary **with their justification**, exactly as in the PR pipeline.
|
||||
- **The result is written down.** Every matrix leg records its own outcome next to the four facts that decide what the outcome means — date, runner label, `K3S_IMAGE`, `kubectl` version — and a final job records the verdict over the whole matrix. A green run nobody notes is unknown again next time. Per leg and not as one table, because a matrix leg only ever sees its own job summary.
|
||||
- **Expected runtime: 20 charts in 12m41s** (Gitea PR #48, one job, all legs concurrently on one Docker daemon). The figure is there so a later deviation is visible as one; compare it against the chart count the run prints in its own summary — 24 at the time of writing, and one more with every new chart. The job timeout is 30 minutes per leg and lives in `install-test.yml`, i.e. it is the same for both callers on purpose.
|
||||
|
||||
## Dependency updates (Renovate)
|
||||
|
||||
Renovate runs as the shared `jlab-renovate-bot` instance on the Gitea side, which also serves the other repositories there — see "Renovate on Gitea" below. **There is no Renovate workflow in this repo, and none should be added.** (The `jlab-renovate` GitHub App that used to do this is switched off along with the rest of GitHub Actions.) What updates is unaffected either way: the config is **`renovate.json5`** in the repo root (JSON5 for the inline reasoning; requires Renovate >= 41 for `managerFilePatterns`/`customType`). There must be exactly one config file — do not add a second `renovate.json` or `.github/renovate.json`.
|
||||
@@ -365,12 +344,6 @@ Reference implementation: `template-chart/values.schema.json` and `template-char
|
||||
|
||||
**2. Probes, security contexts and resources go through the null-safe helper, never behind an `if`.** An override written without children is YAML null and erases the block: the container renders `securityContext: null` (unhardened, healthy, silent), a probe disappears, or `resources` loses its requests and computed limits. The `defaultedDict` helper defines the opposite semantics — an empty block means "chart defaults apply" — while an explicitly set value still wins, **including `false` and `0`** (hence the `kindIs "invalid"` check; `default` and `mergeOverwrite` both swallow zero values). Do **not** wrap these blocks in `{{- if }}`: a guard does not render a visible `null`, it removes the probe entirely, which is the same bug with no evidence left.
|
||||
|
||||
The rule is about the **shape, not about those three block kinds**. `satisfactory` shipped 7.0.0 with `game` null-safe and its two neighbours not (Gitea #16): `--set server=null` aborted the render, and a single erased key was the quieter half — a nulled `server.gamePort` rendered `port:` and `nodePort:` empty into the Service and an empty `containerPort` into the StatefulSet, rejected by nothing until the API server saw it. Every chart-owned block a template dereferences belongs behind the helper, and a chart where only *some* of them do is worse than one where none do: whoever sees `game=null` fall back cleanly assumes `server=null` does too.
|
||||
|
||||
**A deliberate exception is fine; an undocumented one is the bug.** `satisfactory.storage` is left un-defaulted on purpose — an erased size yields an invalid PVC that fails at apply time rather than a volume quietly sized by the chart — so `server: null` still renders a PVC without a size. That is a defensible trade and it is *not* the null-safety bug next to it; what makes it safe to leave standing is that the schema, `values.yaml` and the chart page all name it as an exception and point at the issue where it is argued (Gitea #39). Note also where the trade actually bites: only on a **new** install. For an existing release a fallback to the default size would fail too, because a PVC cannot shrink. Do not overturn a reasoned exception inside a bugfix patch — the patch then stops being one.
|
||||
|
||||
**And `helm template` does not render `NOTES.txt`.** A value read there — `server` is read in four files in that chart — stays broken after a fix that looks green in every render, until someone runs a real `helm install`. `helm install --dry-run=client` is not the way around it either: it still wants a reachable API server (measured on helm v3.22.0). What works without a cluster: in a throwaway copy of the chart, wrap the verbatim `NOTES.txt` body in a `{{- define }}` and `include` it into a scratch ConfigMap, which `helm template` *does* render — same engine, same root context, only the sink differs.
|
||||
|
||||
**3. Do not hang liveness on an external dependency where a dependency-free path exists.** `zipline` deliberately probes `/` for liveness and `/api/healthcheck` only for readiness and startup, so a database outage cannot restart the pod. Where no such path exists — `outline` returns 500 on `/` when the database is down — all three probes may share the endpoint, but the reason belongs in a comment on the helper, or the next reader will "fix" the inconsistency.
|
||||
|
||||
The other half of the same rule: readiness **must** hang on the dependency, and a separate path is not proof that it does. `urlaubsverwaltung` probed `/actuator/health/liveness` and `/actuator/health/readiness` for two years and still stayed 1/1 Ready through a total database outage (#218), because Spring Boot's `readiness` group contains `readinessState` **alone** — the split paths were right, the group content was empty of anything external. Whatever aggregates behind a probe path (Spring health groups, a `/healthz` a framework assembles for you) has to be read off the **running** endpoint, not off the path name; the chart names the members explicitly (`MANAGEMENT_ENDPOINT_HEALTH_GROUP_READINESS_INCLUDE=readinessState,db`) so the composition is in the template rather than in an upstream default.
|
||||
|
||||
@@ -5,5 +5,5 @@ icon: https://www.apache.org/logos/res/tika/default.png
|
||||
|
||||
type: application
|
||||
|
||||
version: 10.0.2+up3.3.1.0.full
|
||||
version: 10.0.1+up3.3.1.0.full
|
||||
appVersion: "3.3.1.0-full"
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "apache-tika.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "apache-tika.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -170,9 +168,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "apache-tika.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/alam00000/bentopdf/main/public/images/fa
|
||||
|
||||
type: application
|
||||
|
||||
version: 1.0.3+up2.8.8
|
||||
version: 1.0.2+up2.8.8
|
||||
appVersion: "2.8.8"
|
||||
|
||||
maintainers:
|
||||
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "bentopdf.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "bentopdf.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -170,9 +168,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "bentopdf.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
@@ -5,7 +5,7 @@ name: calibre-web-automated
|
||||
description: A Helm chart to deploy calibere-web-automated
|
||||
icon: https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/main/README_images/cwa-logo-round-dark.png
|
||||
|
||||
version: 7.0.1+upv4.0.6
|
||||
version: 7.0.0+upv4.0.6
|
||||
appVersion: v4.0.6
|
||||
|
||||
maintainers:
|
||||
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "calibre-web-automated.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "calibre-web-automated.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -169,9 +167,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "calibre-web-automated.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# (REQUIRED_ENV), and pipes the result straight into kubectl.
|
||||
#
|
||||
# The values come from a 1Password item holding a CI-only Mullvad key, loaded in
|
||||
# the install-test job of .github/workflows/install-test.yml. A CI-only key
|
||||
# the install-test job of .github/workflows/validate-charts.yml. A CI-only key
|
||||
# on purpose: Mullvad ties an address to a key and sends the tunnel to the
|
||||
# endpoint it saw last, so a shared key would let every pull request pull the
|
||||
# tunnel out from under the running stack.
|
||||
|
||||
+7
-10
@@ -21,11 +21,9 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
CI_DIR="$ROOT/ci"
|
||||
|
||||
# --- The exclusion list, parsed in exactly one place ------------------------
|
||||
# The consumers of ci/excluded-charts.txt are this script and the two jobs that
|
||||
# build an install-test matrix - detect-changed-charts in
|
||||
# .github/workflows/validate-charts.yml and select-charts in
|
||||
# .github/workflows/install-test-all-charts.yml - both of which drop the
|
||||
# excluded charts before the matrix exists (issue #231). Two parsers would be two
|
||||
# Two consumers read ci/excluded-charts.txt: this script, and the install-test
|
||||
# matrix in .github/workflows/validate-charts.yml, which drops the excluded
|
||||
# charts before the matrix is built (issue #231). Two parsers would be two
|
||||
# chances to select different charts - and the failure would be silent in the
|
||||
# worst direction, a chart the workflow believes excluded and the script
|
||||
# believes testable, or the reverse. So there is one parser, here, and the
|
||||
@@ -49,10 +47,9 @@ excluded_reason() {
|
||||
list_excluded | awk -F'\t' -v chart="$1" '$1 == chart { print $2; exit }'
|
||||
}
|
||||
|
||||
# Emits "<chart><TAB><reason>" for every excluded chart. Read by both jobs that
|
||||
# assemble an install-test matrix (detect-changed-charts in validate-charts.yml,
|
||||
# select-charts in install-test-all-charts.yml) before any chart is picked,
|
||||
# which is why this runs before the argument is required.
|
||||
# Emits "<chart><TAB><reason>" for every excluded chart. Read by the detect job
|
||||
# of validate-charts.yml before any chart is picked, which is why this runs
|
||||
# before the argument is required.
|
||||
if [ "${1:-}" = "--list-excluded" ]; then
|
||||
list_excluded
|
||||
exit 0
|
||||
@@ -103,7 +100,7 @@ fi
|
||||
# REQUIRED_ENV="FREESAILARR_WG_KEY FREESAILARR_WG_ADDRESSES"
|
||||
#
|
||||
# The values come from the environment - in CI from 1Password Connect, see the
|
||||
# install-test job in .github/workflows/install-test.yml.
|
||||
# install-test job in .github/workflows/validate-charts.yml.
|
||||
#
|
||||
# A missing or empty value is a hard error on purpose. Skipping the chart
|
||||
# instead would restore exactly the green-but-meaningless signal this mechanism
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
# Fixtures for the sure install-test: throwaway PostgreSQL 16 (the version
|
||||
# upstream's compose file uses) and Redis, plus the secrets the 1Password
|
||||
# operator would otherwise materialize (release name: ci).
|
||||
#
|
||||
# Both are mandatory for this chart, and for different reasons: the web
|
||||
# container migrates the schema at start and its readiness probe runs a query,
|
||||
# while all three probes of the worker container ask Redis whether the Sidekiq
|
||||
# process still has a heartbeat. A missing Redis therefore does not show up as
|
||||
# "a background job is late" but as a pod that never turns Ready.
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ci-sure-secret
|
||||
stringData:
|
||||
# 64 bytes hex, as upstream's "openssl rand -hex 64" produces. Any value
|
||||
# works for a throwaway database - it is the key the app derives its Active
|
||||
# Record encryption keys from, and nothing here survives the run.
|
||||
secret-key-base: 3f786850e387550fdab836ed7e6dc881de23001b3f786850e387550fdab836ed7e6dc881de23001b3f786850e387550fdab836ed7e6dc881de23001b3f786850
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ci-sure-db-secret
|
||||
stringData:
|
||||
host: postgres
|
||||
port: "5432"
|
||||
database: sure
|
||||
username: ci
|
||||
password: ci
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ci-sure-redis-secret
|
||||
stringData:
|
||||
connection-url: redis://:cipass@redis:6379/0
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: postgres
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: public.ecr.aws/docker/library/postgres:16-alpine
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
value: ci
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: ci
|
||||
- name: POSTGRES_DB
|
||||
value: sure
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
readinessProbe:
|
||||
# -d names the database the chart actually uses. Without it
|
||||
# pg_isready asks for a database named after the user and the
|
||||
# server logs 'FATAL: database "ci" does not exist' every five
|
||||
# seconds - noise that reads like a chart problem in the diagnostics
|
||||
# dump when something else fails.
|
||||
exec:
|
||||
command: ["pg_isready", "-U", "ci", "-d", "sure"]
|
||||
periodSeconds: 5
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
volumes:
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
spec:
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: public.ecr.aws/docker/library/redis:7-alpine
|
||||
args: ["redis-server", "--requirepass", "cipass"]
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: 6379
|
||||
periodSeconds: 5
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
selector:
|
||||
app: redis
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
@@ -1,6 +0,0 @@
|
||||
# The first start of the web container creates and migrates the whole Rails
|
||||
# schema ("./bin/rails db:prepare", run by the image entrypoint) before Puma
|
||||
# binds its port, and the worker container only registers with Sidekiq once
|
||||
# that schema exists. Both startup probes budget 600s for it, so the rollout
|
||||
# wait has to be longer than a single startup budget, not shorter.
|
||||
INSTALL_TIMEOUT=900
|
||||
@@ -1,10 +0,0 @@
|
||||
secrets:
|
||||
appSecretRef: op://ci/sure-app
|
||||
dbSecretRef: op://ci/sure-db
|
||||
redisSecretRef: op://ci/sure-redis
|
||||
|
||||
ingress:
|
||||
domain: sure.ci.local
|
||||
# clusterIssuer is strictly required (no default); the Ingress just has to
|
||||
# apply - there is no cert-manager in the CI cluster.
|
||||
clusterIssuer: ci
|
||||
@@ -24,7 +24,7 @@ misconfigurations:
|
||||
statement: "No CPU limits, on purpose: CPU is compressible, so a limit throttles where the missing limit merely shares - see README 'Resources (auto-limits)'. The chart helper renders a CPU limit only when one is set explicitly."
|
||||
|
||||
- id: AVD-KSV-0020
|
||||
statement: "UIDs are pinned to the value the image expects, not raised above 10000: minecraft-server 1000 (gosu default), teamspeak-server 9987, voucher-vault 33, the matrix-synapse delegation nginx 101, sure 1000 (its Dockerfile declares USER 1000:1000). calibre-web-automated shows the risk - a runtime uid moving from 1000 to 911 makes existing volume data inaccessible, and every chart with a PVC carries that same risk class."
|
||||
statement: "UIDs are pinned to the value the image expects, not raised above 10000: minecraft-server 1000 (gosu default), teamspeak-server 9987, voucher-vault 33, the matrix-synapse delegation nginx 101. calibre-web-automated shows the risk - a runtime uid moving from 1000 to 911 makes existing volume data inaccessible, and every chart with a PVC carries that same risk class."
|
||||
|
||||
- id: AVD-KSV-0021
|
||||
statement: "GIDs follow the pinned UIDs above; the same image expectation and the same PVC ownership risk apply."
|
||||
@@ -109,19 +109,6 @@ misconfigurations:
|
||||
paths: ["**/homeassistant.yaml"]
|
||||
statement: "NET_RAW is the capability in question and is added on purpose for device discovery; see AVD-KSV-0022."
|
||||
|
||||
# --- valkey: the two keyword findings on its ConfigMap ---------------------
|
||||
# Both are keyword matches on the COMMENTS and the shell script in the
|
||||
# ConfigMap, not on data. Worth suppressing with a reason rather than leaving
|
||||
# standing, because "ConfigMap stores secrets" on this chart of all charts
|
||||
# says the opposite of what it does: the whole design exists to keep the
|
||||
# password out of everything Helm renders.
|
||||
- id: AVD-KSV-0109
|
||||
paths: ["**/valkey.yaml"]
|
||||
statement: "False positive on the rendered ConfigMap. The matched strings are the comment '# Nothing here prints the password' and the line pass=\"$(sed -n 's/^requirepass...' \"$AUTH_FILE\")\" of the probe script - i.e. the code that READS the password out of the mounted Secret file at probe time. The ConfigMap contains no credential: the password lives only in the 1Password item, is materialized into a Secret by the operator, mounted as a file and 'include'd by the configuration, which is precisely why no requirepass line is rendered here. Verify by rendering the chart - no value from the vault appears in it."
|
||||
- id: AVD-KSV-01010
|
||||
paths: ["**/valkey.yaml"]
|
||||
statement: "Same class as AVD-KSV-0109 above: the matched keys are 'valkey.conf' and an 'echo \"valkey...' line of the validation script. Both are chart-authored configuration and diagnostics text; nothing user- or credential-derived is stored in this ConfigMap."
|
||||
|
||||
# --- rss-bridge: port 80 is fixed by the image ----------------------------
|
||||
- id: AVD-KSV-0117
|
||||
paths: ["**/rss-bridge.yaml"]
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Fixtures for the valkey install-test: the one Secret the 1Password operator
|
||||
# would otherwise materialize from the OnePasswordItem the chart renders
|
||||
# (release name: ci, stub CRD in ci/common/).
|
||||
#
|
||||
# The field name matters as much as the value. The chart mounts exactly the key
|
||||
# "auth-conf" and its content has to be a COMPLETE Valkey configuration line,
|
||||
# because the configuration "include"s the file - so this fixture is also what
|
||||
# keeps the init container's check honest: give it anything that is not a
|
||||
# "requirepass <password>" line and the pod refuses to start, which is the
|
||||
# failure this chart exists to make loud.
|
||||
#
|
||||
# No throwaway infrastructure here: Valkey depends on nothing. What this
|
||||
# install-test does exercise, and why it is the persistent variant, is the risky
|
||||
# path - a provisioned PVC, the pod's fsGroup against it, and an append-only
|
||||
# file being created and replayed on a volume the container did not create.
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ci-valkey-secret
|
||||
stringData:
|
||||
# A throwaway password. Nothing here survives the run - but it is a real
|
||||
# password rather than an empty string on purpose: an empty one would produce
|
||||
# exactly the open server the chart refuses to start, and the run would fail
|
||||
# for the right reason at the wrong place.
|
||||
auth-conf: requirepass ci-valkey-throwaway-password
|
||||
@@ -1,37 +0,0 @@
|
||||
# CI values for the valkey install-test. The PERSISTENT variant on purpose: it is
|
||||
# the chart's default and the only risky path there is (a provisioned PVC, the
|
||||
# pod's fsGroup against it, an append-only file created on a volume the container
|
||||
# did not create). The machinery can run one variant per chart, so the
|
||||
# non-persistent path is covered by a local render only - named as a known gap in
|
||||
# the PR that introduced this chart.
|
||||
secrets:
|
||||
authSecretRef: op://ci/valkey
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
appendonly: true
|
||||
# k3d's dynamic provisioner. The chart's default, longhorn-encrypted-ns, does
|
||||
# not exist on the runner, and a pre-bound hostPath PV would be worse than this
|
||||
# one: the kubelet does not apply fsGroup to a hostPath volume at all, so the
|
||||
# test would silently stop exercising the ownership path it is here for.
|
||||
storageClass: local-path
|
||||
size: 128Mi
|
||||
|
||||
valkey:
|
||||
# Small, so the whole thing stays inside the runner's budget. It still
|
||||
# exercises the derivation: 64Mi x limitFactor 3 = 192Mi limit, maxmemory =
|
||||
# 0.75 x that = 144Mi - and the render aborts if a configured maxmemory ever
|
||||
# ends up above the limit.
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
ephemeral-storage: 32Mi
|
||||
|
||||
# Left ON, although nothing in the test connects from another namespace: the
|
||||
# point here is that the object is accepted and does not keep the pod's own
|
||||
# traffic (the exec probes are local to the pod and unaffected) from working.
|
||||
# Whether the policy actually FILTERS is a property of the cluster's CNI and is
|
||||
# not claimed by this test.
|
||||
networkPolicy:
|
||||
enabled: true
|
||||
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/gchq/CyberChef/master/src/web/static/ima
|
||||
|
||||
type: application
|
||||
|
||||
version: 9.0.2+up11.4.0
|
||||
version: 9.0.1+up11.4.0
|
||||
appVersion: "11.4.0"
|
||||
|
||||
maintainers:
|
||||
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "cyberchef.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "cyberchef.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -170,9 +168,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "cyberchef.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
@@ -8,7 +8,7 @@ description: A Helm chart to deploy a Wireguard-tunneled media automation stack
|
||||
|
||||
type: application
|
||||
|
||||
version: 3.1.1+up1.0.0
|
||||
version: 3.1.0+up1.0.0
|
||||
# The chart's own version marker, freely chosen and tied to no upstream. The
|
||||
# stack bundles ten independently versioned projects (nine of them on by
|
||||
# default), so there is no single "app version" to track here.
|
||||
|
||||
@@ -19,10 +19,9 @@ Usage:
|
||||
resources:
|
||||
{{- include "freesailarr.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. The reference implementation lives in template-chart.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. The reference implementation lives in template-chart.
|
||||
*/}}
|
||||
{{- define "freesailarr.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
|
||||
@@ -5,7 +5,7 @@ icon: https://gotenberg.dev/img/logo.png
|
||||
|
||||
type: application
|
||||
|
||||
version: 8.0.3+up8.37.0
|
||||
version: 8.0.2+up8.37.0
|
||||
appVersion: "8.37.0"
|
||||
|
||||
maintainers:
|
||||
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "gotenberg.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "gotenberg.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -170,9 +168,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "gotenberg.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
@@ -4,7 +4,7 @@ description: A Helm chart to deploy Home Assistant on Kubernetes
|
||||
icon: https://www.home-assistant.io/images/home-assistant-logo.svg
|
||||
type: application
|
||||
|
||||
version: 6.1.1+up2026.8.3
|
||||
version: 6.1.0+up2026.8.3
|
||||
|
||||
appVersion: "2026.8.3"
|
||||
|
||||
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "homeassistant.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "homeassistant.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -169,9 +167,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "homeassistant.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
@@ -3,7 +3,7 @@ name: matrix-synapse
|
||||
description: A Helm chart to deploy a Matrix Synapse server on Kubernetes
|
||||
icon: https://raw.githubusercontent.com/element-hq/synapse/develop/docs/favicon.png
|
||||
type: application
|
||||
version: 7.1.1+upv1.159.0
|
||||
version: 7.1.0+upv1.159.0
|
||||
appVersion: "v1.159.0"
|
||||
maintainers:
|
||||
- name: jklein
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "matrix-synapse.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "matrix-synapse.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -170,9 +168,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "matrix-synapse.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
@@ -5,8 +5,8 @@ icon: https://avatars.githubusercontent.com/u/988985
|
||||
|
||||
type: application
|
||||
|
||||
version: 4.1.3+up1.47.1
|
||||
appVersion: "1.47.1"
|
||||
version: 4.1.1+up1.46.5
|
||||
appVersion: "1.46.5"
|
||||
|
||||
maintainers:
|
||||
- name: jklein
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "minecraft-router.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "minecraft-router.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -170,9 +168,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "minecraft-router.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
@@ -23,7 +23,7 @@ type: application
|
||||
# continuously, so a pod restart could pull a different image than the last one
|
||||
# without anything being recorded anywhere. The dated tags are immutable, so the
|
||||
# image is now pinned by the chart version like every other chart in this repo.
|
||||
version: 6.0.1+up2026.8.2.java25
|
||||
version: 6.0.0+up2026.8.2.java25
|
||||
appVersion: "2026.8.2-java25"
|
||||
|
||||
maintainers:
|
||||
|
||||
@@ -19,10 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "minecraft-server.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. The reference implementation lives in template-chart.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. The reference implementation lives in template-chart.
|
||||
*/}}
|
||||
{{- define "minecraft-server.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -347,9 +346,9 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14). The reference implementation lives in template-chart.
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies. The reference implementation
|
||||
lives in template-chart.
|
||||
*/}}
|
||||
{{- define "minecraft-server.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/outline/outline/main/public/images/icon-
|
||||
|
||||
type: application
|
||||
|
||||
version: 7.1.3+up1.10.1
|
||||
version: 7.1.2+up1.10.1
|
||||
appVersion: 1.10.1
|
||||
|
||||
maintainers:
|
||||
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "outline.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "outline.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -170,9 +168,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "outline.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
@@ -5,9 +5,9 @@ icon: https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/dev/src-ui/s
|
||||
|
||||
type: application
|
||||
|
||||
version: 12.1.3+up3.2.1
|
||||
version: 12.1.1+up3.1.0
|
||||
|
||||
appVersion: 3.2.1
|
||||
appVersion: 3.1.0
|
||||
|
||||
maintainers:
|
||||
- name: jklein
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "paperless-ngx.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "paperless-ngx.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/PatchMon/PatchMon/main/frontend/public/a
|
||||
|
||||
type: application
|
||||
|
||||
version: 7.0.2+up2.1.3
|
||||
version: 7.0.1+up2.1.3
|
||||
appVersion: "2.1.3"
|
||||
|
||||
maintainers:
|
||||
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "patchmon.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "patchmon.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -170,9 +168,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "patchmon.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@ icon: https://raw.githubusercontent.com/lukevella/rallly/main/apps/landing/publi
|
||||
|
||||
type: application
|
||||
|
||||
version: 8.1.6+up4.15.2
|
||||
appVersion: "4.15.2"
|
||||
version: 8.1.4+up4.15.1
|
||||
appVersion: "4.15.1"
|
||||
|
||||
maintainers:
|
||||
- name: jklein
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "rallly.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "rallly.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -170,9 +168,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "rallly.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
+1
-61
@@ -41,7 +41,7 @@
|
||||
prHourlyLimit: 0,
|
||||
|
||||
// Minor and patch arrive as separate PRs rather than one combined branch.
|
||||
// With 24 charts a shared branch means one red install-test blocks unrelated
|
||||
// With 23 charts a shared branch means one red install-test blocks unrelated
|
||||
// updates, and the separation is what lets a patch be reviewed in seconds.
|
||||
separateMinorPatch: true,
|
||||
|
||||
@@ -485,33 +485,6 @@
|
||||
datasourceTemplate: "docker",
|
||||
depTypeTemplate: "app-version",
|
||||
},
|
||||
{
|
||||
description: [
|
||||
"sure: the registry carries four kinds of tag side by side - releases (0.7.4), the",
|
||||
"prereleases -alpha.N / -rc.N / -hotfix.N, the moving names stable/latest/nightly,",
|
||||
"and one sha-<commit> per build. semver versioning is the whole filter: it parses",
|
||||
"the release line, marks every prerelease isStable === false so `ignoreUnstable`",
|
||||
"never offers one, and rejects the moving names and the sha- tags as unparseable",
|
||||
"rather than ever selecting one. That matters more here than the tag list suggests,",
|
||||
"because automerge is global in this config (top of the file) and majors carry no",
|
||||
"age gate: an update this manager offers is one that merges as soon as the required",
|
||||
"check and the required review are there, so the filtering has to happen at the",
|
||||
"lookup, not at the merge. It is also the reason NOT to write a capture-group regex",
|
||||
"here - the moving tags have to stay outside whatever pattern is used, and 'outside'",
|
||||
"is free with semver.",
|
||||
"",
|
||||
"Upstream calls 'latest' the alpha channel and 'stable' the latest release; at the",
|
||||
"time of writing 'stable' resolves to the same digest as 0.7.4, so pinning the",
|
||||
"version tag loses nothing and keeps the chart's appVersion honest.",
|
||||
],
|
||||
customType: "regex",
|
||||
managerFilePatterns: ["sure/Chart.yaml"],
|
||||
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
|
||||
depNameTemplate: "ghcr.io/we-promise/sure",
|
||||
datasourceTemplate: "docker",
|
||||
depTypeTemplate: "app-version",
|
||||
versioningTemplate: "semver",
|
||||
},
|
||||
{
|
||||
description: "teamspeak-server: the 6.0.0 line only ships beta tags (6.0.0-beta12.1). Loose versioning compares the numeric parts left to right, so beta12.1 sorts above beta9; semver would rank every beta as an equal prerelease.",
|
||||
customType: "regex",
|
||||
@@ -538,39 +511,6 @@
|
||||
datasourceTemplate: "docker",
|
||||
depTypeTemplate: "app-version",
|
||||
},
|
||||
{
|
||||
description: [
|
||||
"valkey: the simplest block in this file, and deliberately so. docker versioning",
|
||||
"splits the tag at the first dash, reads 9.1.2 as the version and 'alpine' as the",
|
||||
"compatibility suffix, and only ever offers candidates carrying that same suffix -",
|
||||
"which is exactly what is wanted here, because upstream keeps building an -alpine",
|
||||
"tag for every release. No versioningTemplate, no capture groups, nothing to prove.",
|
||||
"",
|
||||
"That is the SECOND design for this block. The first pinned the fully numbered",
|
||||
"9.1.2-alpine3.24 and needed a regex versioning to see a move to alpine3.25, since",
|
||||
"upstream builds exactly one Alpine base per release and the next base arrives as",
|
||||
"9.1.3-alpine3.25 rather than 9.1.2-alpine3.25. It was dropped because the premise",
|
||||
"was wrong: that tag is not immutable either - upstream rebuilds the whole matrix",
|
||||
"periodically (9.1-alpine and 9.0.6-alpine3.24 were pushed minutes apart on",
|
||||
"2026-09-21, two different release lines). Only a digest is immutable. Given two",
|
||||
"equally moving tags, the one whose suffix never changes is the safer pin, and its",
|
||||
"rule needs no proof - which mattered, because a Renovate dry run is impossible in",
|
||||
"the work environment (no node, see #34).",
|
||||
"",
|
||||
"The price, stated rather than discovered later: a rebuild of 9.1.2-alpine changes",
|
||||
"the running image with no commit here and nothing reports it. The remedy exists if",
|
||||
"it ever matters - pinDigests for this one image. It is off globally for",
|
||||
"custom.regex since #34 because an appVersion is a tag by construction, but a",
|
||||
"per-package exception is possible, and it would then be a chart patch plus a",
|
||||
"publish per digest change.",
|
||||
],
|
||||
customType: "regex",
|
||||
managerFilePatterns: ["valkey/Chart.yaml"],
|
||||
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
|
||||
depNameTemplate: "docker.io/valkey/valkey",
|
||||
datasourceTemplate: "docker",
|
||||
depTypeTemplate: "app-version",
|
||||
},
|
||||
{
|
||||
description: [
|
||||
"ytdlp-web-player: the registry carries exactly two kinds of tag - four versioned",
|
||||
|
||||
+1
-1
@@ -5,6 +5,6 @@ icon: https://avatars.githubusercontent.com/u/13281359
|
||||
|
||||
type: application
|
||||
|
||||
version: 7.1.1+up4.23.10
|
||||
version: 7.1.0+up4.23.10
|
||||
|
||||
appVersion: 4.23.10
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "samba.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "samba.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -169,9 +167,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "samba.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
@@ -3,7 +3,7 @@ name: satisfactory
|
||||
description: A Helm chart to deploy a Satisfactory Gameserver on Kubernetes
|
||||
icon: https://raw.githubusercontent.com/wolveix/satisfactory-server/main/.github/logo.png
|
||||
type: application
|
||||
version: 7.0.2+upv1.9.10
|
||||
version: 7.0.0+upv1.9.10
|
||||
appVersion: "v1.9.10"
|
||||
maintainers:
|
||||
- name: jklein
|
||||
@@ -1,19 +1,17 @@
|
||||
{{- $server := fromYaml (include "satisfactory.server" .Values.server) -}}
|
||||
{{- $dev := fromYaml (include "satisfactory.dev" .Values.dev) -}}
|
||||
{{- $replicas := include "satisfactory.replicas" (dict "scaleDown" .Values.scaleDown "replicas" (include "satisfactory.formatNumber" $server.replicas)) -}}
|
||||
{{- $gamePort := int $server.gamePort -}}
|
||||
{{- $msgPort := int $server.messagingPort -}}
|
||||
{{- $replicas := include "satisfactory.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.server.replicas) -}}
|
||||
{{- $gamePort := int .Values.server.gamePort -}}
|
||||
{{- $msgPort := int .Values.server.messagingPort -}}
|
||||
{{- $svc := printf "%s-%s-service" .Release.Name .Chart.Name -}}
|
||||
{{- $sfs := printf "%s-%s-sfs" .Release.Name .Chart.Name -}}
|
||||
{{- $pod := printf "%s-%s-sfs-0" .Release.Name .Chart.Name -}}
|
||||
{{- $pvc := printf "%s-%s-vc" .Release.Name .Chart.Name -}}
|
||||
{{- $storage := $server.storage | default "unset" -}}
|
||||
{{- $res := fromYaml (include "satisfactory.effectiveResources" $server.resources) -}}
|
||||
{{- $storage := .Values.server.storage | default "unset" -}}
|
||||
{{- $res := fromYaml (include "satisfactory.effectiveResources" .Values.server.resources) -}}
|
||||
{{- $lim := $res.limits | default dict -}}
|
||||
{{- $req := $res.requests | default dict -}}
|
||||
{{- $eph := index $lim "ephemeral-storage" | default "the computed ephemeral-storage limit" -}}
|
||||
{{- $mem := index $lim "memory" | default "the computed memory limit" -}}
|
||||
{{- $factor := ($server.resources | default dict).limitFactor | default 2 -}}
|
||||
{{- $factor := (.Values.server.resources | default dict).limitFactor | default 2 -}}
|
||||
{{- $startup := fromYaml (include "satisfactory.startupProbe" .Values.startupProbe) -}}
|
||||
{{- $budget := "" -}}
|
||||
{{- if and (kindIs "float64" $startup.periodSeconds) (kindIs "float64" $startup.failureThreshold) -}}
|
||||
@@ -23,7 +21,7 @@
|
||||
{{- $seasonalEnv := include "satisfactory.disableSeasonalEvents" $game.enableSeasonalEvents -}}
|
||||
{{- $seasonalOff := eq $seasonalEnv "true" -}}
|
||||
{{- $beta := eq (lower (printf "%v" $game.experimental)) "true" -}}
|
||||
{{- $debug := eq (lower (printf "%v" $dev.debug)) "true" -}}
|
||||
{{- $debug := eq (lower (printf "%v" .Values.dev.debug)) "true" -}}
|
||||
{{- $outOfRange := or (lt $gamePort 30000) (gt $gamePort 32767) (lt $msgPort 30000) (gt $msgPort 32767) -}}
|
||||
{{- $skipRaw := "" -}}
|
||||
{{- range $entry := .Values.env -}}
|
||||
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "satisfactory.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "satisfactory.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -170,9 +168,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "satisfactory.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
@@ -311,63 +308,6 @@ DISABLESEASONALEVENTS at "true", the value this chart has always rendered.
|
||||
{{- include "satisfactory.defaultedDict" (dict "defaults" $defaults "given" . "path" "game") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The effective dev block (Gitea issue #16, the rest of #99).
|
||||
|
||||
"--set dev=null" — or a leftover "dev:" whose last sub-key was deleted — used
|
||||
to abort the render with a nil-pointer error at .Values.dev.debug, in the
|
||||
StatefulSet and in NOTES.txt. It now falls back to the default values.yaml
|
||||
documents, the same way the game block has since 7.0.0.
|
||||
|
||||
The defaults below MUST stay in sync with the dev block in values.yaml, which
|
||||
stays the documented, user-facing place for them; this copy is only the
|
||||
fallback for the case where an override has erased the block.
|
||||
*/}}
|
||||
{{- define "satisfactory.dev" -}}
|
||||
{{- $defaults := dict
|
||||
"debug" false -}}
|
||||
{{- include "satisfactory.defaultedDict" (dict "defaults" $defaults "given" . "path" "dev") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The effective server block (Gitea issue #16, the rest of #99).
|
||||
|
||||
"--set server=null" used to abort the render at .Values.server.replicas, and a
|
||||
single erased sub-key was worse than that: it rendered silently. A nulled
|
||||
server.gamePort produced a Service with "port:" and "nodePort:" empty and a
|
||||
StatefulSet with an empty containerPort — a manifest the API server rejects
|
||||
only at apply time. Those now fall back to the documented defaults instead.
|
||||
|
||||
TWO keys are deliberately NOT defaulted here.
|
||||
|
||||
server.resources, because it already runs through
|
||||
satisfactory.effectiveResources, which carries its own defaults and is
|
||||
null-safe on its own; repeating limitFactor/4Gi/1/5Gi here would create a
|
||||
third place for them to drift apart.
|
||||
|
||||
server.storage, because values.schema.json documents the opposite decision
|
||||
for it on purpose: an erased size must yield an invalid PVC that fails at
|
||||
apply time rather than a workload quietly sized by the chart. An erased
|
||||
"server:" therefore still renders a PVC without a size — deliberately, not by
|
||||
oversight. That exception is worth re-examining, but it is someone's reasoned
|
||||
decision and not a bugfix's to overturn; it is negotiated in Gitea issue #39.
|
||||
|
||||
Both rely on the same mechanic: keys the defaults do not mention are passed
|
||||
through by satisfactory.defaultedDict unchanged, so an erased "server:" leaves
|
||||
them unset and whatever consumes them downstream decides what that means.
|
||||
|
||||
The defaults below MUST stay in sync with the server block in values.yaml,
|
||||
which stays the documented, user-facing place for them; this copy is only the
|
||||
fallback for the case where an override has erased the block.
|
||||
*/}}
|
||||
{{- define "satisfactory.server" -}}
|
||||
{{- $defaults := dict
|
||||
"replicas" 1
|
||||
"gamePort" 30777
|
||||
"messagingPort" 30888 -}}
|
||||
{{- include "satisfactory.defaultedDict" (dict "defaults" $defaults "given" . "path" "server") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render the DISABLESEASONALEVENTS env value from game.enableSeasonalEvents
|
||||
(issue #215).
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
{{- $server := fromYaml (include "satisfactory.server" .Values.server) -}}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
@@ -8,5 +7,5 @@ spec:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ $server.storage }}
|
||||
storage: {{ .Values.server.storage }}
|
||||
storageClassName: longhorn
|
||||
@@ -1,6 +1,3 @@
|
||||
{{- $server := fromYaml (include "satisfactory.server" .Values.server) -}}
|
||||
{{- $gamePort := include "satisfactory.formatNumber" $server.gamePort -}}
|
||||
{{- $msgPort := include "satisfactory.formatNumber" $server.messagingPort -}}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
@@ -11,19 +8,19 @@ spec:
|
||||
type: NodePort
|
||||
ports:
|
||||
- name: "api"
|
||||
port: {{ $gamePort }}
|
||||
port: {{ .Values.server.gamePort }}
|
||||
protocol: TCP
|
||||
nodePort: {{ $gamePort }}
|
||||
targetPort: {{ $gamePort }}
|
||||
nodePort: {{ .Values.server.gamePort }}
|
||||
targetPort: {{ .Values.server.gamePort }}
|
||||
- name: "game"
|
||||
port: {{ $gamePort }}
|
||||
port: {{ .Values.server.gamePort }}
|
||||
protocol: UDP
|
||||
nodePort: {{ $gamePort }}
|
||||
targetPort: {{ $gamePort }}
|
||||
nodePort: {{ .Values.server.gamePort }}
|
||||
targetPort: {{ .Values.server.gamePort }}
|
||||
- name: "messaging"
|
||||
port: {{ $msgPort }}
|
||||
port: {{ .Values.server.messagingPort }}
|
||||
protocol: TCP
|
||||
nodePort: {{ $msgPort }}
|
||||
targetPort: {{ $msgPort }}
|
||||
nodePort: {{ .Values.server.messagingPort }}
|
||||
targetPort: {{ .Values.server.messagingPort }}
|
||||
selector:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
@@ -1,6 +1,4 @@
|
||||
{{- $game := fromYaml (include "satisfactory.game" .Values.game) -}}
|
||||
{{- $dev := fromYaml (include "satisfactory.dev" .Values.dev) -}}
|
||||
{{- $server := fromYaml (include "satisfactory.server" .Values.server) -}}
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
@@ -12,7 +10,7 @@ spec:
|
||||
matchLabels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
serviceName: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
replicas: {{ include "satisfactory.replicas" (dict "scaleDown" .Values.scaleDown "replicas" (include "satisfactory.formatNumber" $server.replicas)) }}
|
||||
replicas: {{ include "satisfactory.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.server.replicas) }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
@@ -26,7 +24,7 @@ spec:
|
||||
image: "ghcr.io/wolveix/satisfactory-server:{{ .Chart.AppVersion }}"
|
||||
env:
|
||||
- name: DEBUG
|
||||
value: "{{ $dev.debug }}"
|
||||
value: "{{ .Values.dev.debug }}"
|
||||
- name: MAXPLAYERS
|
||||
value: "{{ include "satisfactory.formatNumber" $game.maxPlayers }}"
|
||||
# PUID/PGID are only used by the image's root entrypoint path;
|
||||
@@ -44,9 +42,9 @@ spec:
|
||||
- name: DISABLESEASONALEVENTS
|
||||
value: "{{ include "satisfactory.disableSeasonalEvents" $game.enableSeasonalEvents }}"
|
||||
- name: SERVERGAMEPORT
|
||||
value: "{{ include "satisfactory.formatNumber" $server.gamePort }}"
|
||||
value: "{{ .Values.server.gamePort }}"
|
||||
- name: SERVERMESSAGINGPORT
|
||||
value: "{{ include "satisfactory.formatNumber" $server.messagingPort }}"
|
||||
value: "{{ .Values.server.messagingPort }}"
|
||||
{{- range $value := .Values.env }}
|
||||
{{- if $value.value }}
|
||||
- name: {{ $value.name }}
|
||||
@@ -69,13 +67,13 @@ spec:
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: "api"
|
||||
containerPort: {{ include "satisfactory.formatNumber" $server.gamePort }}
|
||||
containerPort: {{ .Values.server.gamePort }}
|
||||
protocol: TCP
|
||||
- name: "game"
|
||||
containerPort: {{ include "satisfactory.formatNumber" $server.gamePort }}
|
||||
containerPort: {{ .Values.server.gamePort }}
|
||||
protocol: UDP
|
||||
- name: "messaging"
|
||||
containerPort: {{ include "satisfactory.formatNumber" $server.messagingPort }}
|
||||
containerPort: {{ .Values.server.messagingPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
{{- include "satisfactory.livenessProbe" .Values.livenessProbe | nindent 12 }}
|
||||
@@ -84,7 +82,7 @@ spec:
|
||||
startupProbe:
|
||||
{{- include "satisfactory.startupProbe" .Values.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "satisfactory.effectiveResources" $server.resources | nindent 12 }}
|
||||
{{- include "satisfactory.effectiveResources" .Values.server.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: satisfactory-volume
|
||||
mountPath: /config
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||
"title": "satisfactory values",
|
||||
"description": "Strict values schema (issue #68). additionalProperties is false on every chart-owned object level, so a key that the chart does not read is rejected at render time instead of being silently ignored. The only objects that stay open are the ones passed verbatim into the Kubernetes pod spec (probes, security contexts, global) - the chart does not interpret their contents, so it cannot judge which keys are meaningful. The hardening, probe and resources blocks additionally accept null, because an erased block falls back to the chart defaults (issue #99); since 7.0.1 that holds for every chart-owned block, dev, game and server included (Gitea issue #16), so no block aborts the render any more. The one key that stays un-defaulted is server.storage, and it says so itself (Gitea issue #39). Note that this chart keeps env, probes and securityContext at the TOP level rather than under a chart-named key.",
|
||||
"description": "Strict values schema (issue #68). additionalProperties is false on every chart-owned object level, so a key that the chart does not read is rejected at render time instead of being silently ignored. The only objects that stay open are the ones passed verbatim into the Kubernetes pod spec (probes, security contexts, global) - the chart does not interpret their contents, so it cannot judge which keys are meaningful. The hardening, probe and resources blocks additionally accept null, because an erased block falls back to the chart defaults (issue #99). Note that this chart keeps env, probes and securityContext at the TOP level rather than under a chart-named key.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -13,12 +13,11 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"dev": {
|
||||
"description": "May be null / empty: the chart then falls back to its documented dev defaults (Gitea issue #16, the rest of issue #99) instead of aborting the render with a nil-pointer error, which is what an erased block used to do.",
|
||||
"type": ["object", "null"],
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"debug": {
|
||||
"description": "Passed to the image as DEBUG. Null means the chart default (false).",
|
||||
"description": "Passed to the image as DEBUG.",
|
||||
"type": ["boolean", "string", "null"]
|
||||
}
|
||||
}
|
||||
@@ -42,29 +41,25 @@
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"description": "May be null / empty: the chart then falls back to its documented server defaults (Gitea issue #16, the rest of issue #99) instead of aborting the render with a nil-pointer error, which is what an erased block used to do. Two keys are exceptions. server.resources is not defaulted here but passed through to the resources fallback, which carries the defaults for it. server.storage is not defaulted at all, deliberately - see its own description.",
|
||||
"type": ["object", "null"],
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"replicas": {
|
||||
"description": "Null means the chart default (1). An explicit 0 is kept and scales the StatefulSet down.",
|
||||
"type": ["integer", "null"],
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"gamePort": {
|
||||
"description": "Null means the chart default (30777). Up to 7.0.0 an erased port rendered an empty port number into the Service and the StatefulSet, which only the API server rejected (Gitea issue #16).",
|
||||
"type": ["integer", "null"],
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"messagingPort": {
|
||||
"description": "Null means the chart default (30888). Same erased-port trap as gamePort above.",
|
||||
"type": ["integer", "null"],
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"storage": {
|
||||
"description": "Size of the data PVC. Not null-safe on purpose: an erased value yields an invalid PVC rather than a silently degraded workload. This stays the documented exception after Gitea issue #16 made every other block in this chart null-safe, so \"server: null\" still renders a PVC without a size; whether failing loudly at apply time beats falling back to 20Gi is negotiated in Gitea issue #39, not inside a bugfix patch.",
|
||||
"description": "Size of the data PVC. Not null-safe on purpose: an erased value yields an invalid PVC rather than a silently degraded workload.",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"resources": {
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
# Services stay in place; scaleDown: false brings the workloads back up.
|
||||
scaleDown: false
|
||||
|
||||
# Erasing this block (dev: with no sub-keys, or --set dev=null) falls back to
|
||||
# the defaults below instead of aborting the render — same for game, server and
|
||||
# the hardening/probe/resources blocks (Gitea issue #16, issue #99).
|
||||
dev:
|
||||
debug: false
|
||||
|
||||
@@ -31,23 +28,12 @@ game:
|
||||
# again. Set enableSeasonalEvents: true to actually switch the events on.
|
||||
enableSeasonalEvents: false
|
||||
|
||||
# Erasing this block, or any single key in it, falls back to the defaults below
|
||||
# (Gitea issue #16) — with the documented exception of storage, see there.
|
||||
# Before 7.0.1 "server: null" aborted the render, and a single erased key was
|
||||
# the quieter half: a nulled gamePort rendered an empty port number into the
|
||||
# Service and the StatefulSet, rejected by the API server at apply time and by
|
||||
# nothing before it.
|
||||
server:
|
||||
# Replica count for this workload; 0 is allowed and scales it down.
|
||||
# The chart-global scaleDown flag takes precedence.
|
||||
replicas: 1
|
||||
gamePort: 30777
|
||||
messagingPort: 30888
|
||||
# The size of the data PVC, and the ONE key of this block that is NOT
|
||||
# null-safe — deliberately, not by oversight. An erased storage (including
|
||||
# "server: null") renders a PVC without a size, which fails at apply time
|
||||
# instead of quietly sizing the volume from the chart. Whether that is still
|
||||
# the right trade is negotiated in Gitea issue #39, not here.
|
||||
storage: 20Gi
|
||||
resources:
|
||||
# Limits are optional: memory and ephemeral-storage limits default to
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -1,12 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: sure
|
||||
description: A Helm chart to deploy Sure (we-promise) on Kubernetes
|
||||
icon: https://raw.githubusercontent.com/we-promise/sure/main/public/android-chrome-512x512.png
|
||||
|
||||
type: application
|
||||
|
||||
version: 1.0.0+up0.7.4
|
||||
appVersion: "0.7.4"
|
||||
|
||||
maintainers:
|
||||
- name: jklein
|
||||
@@ -1,165 +0,0 @@
|
||||
{{- $replicas := include "sure.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.sure.replicas) -}}
|
||||
{{- $webStartup := fromYaml (include "sure.webStartupProbe" .Values.sure.web.startupProbe) -}}
|
||||
{{- $workerStartup := fromYaml (include "sure.workerStartupProbe" .Values.sure.worker.startupProbe) -}}
|
||||
{{- $workerLiveness := fromYaml (include "sure.workerLivenessProbe" .Values.sure.worker.livenessProbe) -}}
|
||||
Sure {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
||||
|
||||
{{- if .Values.ingress.enabled }}
|
||||
|
||||
ACCESS
|
||||
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 first account created on a fresh instance becomes the super admin — sign
|
||||
in yourself before handing the URL to anyone.
|
||||
{{- else }}
|
||||
|
||||
ACCESS — NO INGRESS
|
||||
ingress.enabled is false, so this release has no Ingress and no certificate.
|
||||
ingress.domain is still required: it becomes APP_DOMAIN (the host in every
|
||||
mail link) and the WebAuthn relying party the app binds passkeys to. Reach
|
||||
the instance inside the cluster with
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80
|
||||
|
||||
and open http://localhost:8080. With the Ingress off the chart also renders
|
||||
RAILS_ASSUME_SSL and RAILS_FORCE_SSL as "false", which is what makes that
|
||||
plain-HTTP session able to log in at all; mail links still point at
|
||||
https://{{ .Values.ingress.domain }}/.
|
||||
{{- end }}
|
||||
{{ if eq $replicas "0" }}
|
||||
SCALED TO ZERO
|
||||
{{ if .Values.scaleDown }}scaleDown: true{{ else }}sure.replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so neither the web nor
|
||||
the worker process is running. PVC, Secrets, Service{{ if .Values.ingress.enabled }}, Ingress and the
|
||||
certificate stay{{ else }} stay{{ end }} in place; set it back and upgrade to bring both containers up.
|
||||
{{ end }}
|
||||
WHAT THE THREE 1PASSWORD ITEMS MUST CONTAIN
|
||||
The chart creates OnePasswordItem CRs; the operator turns each into a
|
||||
Kubernetes Secret whose keys are the FIELD NAMES of the 1Password item. A
|
||||
missing field does not fail the install — the release goes through and the
|
||||
pod then hangs in CreateContainerConfigError naming the key it could not
|
||||
find (kubectl -n {{ .Release.Namespace }} describe pod).
|
||||
|
||||
secrets.appSecretRef → {{ .Values.secrets.appSecretRef }}
|
||||
Secret {{ .Release.Name }}-{{ .Chart.Name }}-secret, field:
|
||||
secret-key-base SECRET_KEY_BASE, 64 random bytes hex
|
||||
(openssl rand -hex 64)
|
||||
|
||||
secrets.dbSecretRef → {{ .Values.secrets.dbSecretRef }}
|
||||
Secret {{ .Release.Name }}-{{ .Chart.Name }}-db-secret, fields:
|
||||
host DB_HOST, the PostgreSQL hostname
|
||||
port DB_PORT, usually 5432
|
||||
database POSTGRES_DB
|
||||
username POSTGRES_USER
|
||||
password POSTGRES_PASSWORD
|
||||
|
||||
secrets.redisSecretRef → {{ .Values.secrets.redisSecretRef }}
|
||||
Secret {{ .Release.Name }}-{{ .Chart.Name }}-redis-secret, field:
|
||||
connection-url REDIS_URL, e.g. redis://:password@host:6379/0
|
||||
|
||||
ROTATING SECRET_KEY_BASE DESTROYS MORE THAN SESSIONS
|
||||
In self-hosted mode Sure derives its Active Record encryption keys from
|
||||
SECRET_KEY_BASE (config/initializers/active_record_encryption.rb: three
|
||||
SHA-256 digests over that one value) unless
|
||||
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY and its two companions are set. Every
|
||||
encrypted column in the database — provider tokens, OAuth credentials, MFA
|
||||
secrets — is therefore readable only with the SECRET_KEY_BASE it was written
|
||||
under. Replacing that value does not just log everyone out: it makes the
|
||||
existing encrypted data undecryptable, and no rotation of the item brings it
|
||||
back. Treat this field as permanent for the life of the database, or pin the
|
||||
three encryption keys explicitly through sure.web.env / sure.worker.env
|
||||
before the first start.
|
||||
|
||||
CHANGING A SECRET'S CONTENT RESTARTS NOTHING
|
||||
Helm never sees the content of a OnePasswordItem, so it cannot hash it. The
|
||||
pod template carries checksum/secret-ref annotations over the rendered
|
||||
OnePasswordItems, so pointing a *SecretRef at a DIFFERENT item rolls the
|
||||
pod — but editing a value INSIDE the same item does not: the operator
|
||||
updates the Kubernetes Secret while both containers keep the old value in
|
||||
their environment. After such a change:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} rollout restart statefulset/{{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
|
||||
The operator's auto-restart annotation would not help here and is
|
||||
deliberately not set: it restarts Deployments only, and this is a
|
||||
StatefulSet.
|
||||
|
||||
POSTGRES AND REDIS ARE NOT PART OF THIS CHART
|
||||
Both must exist and be reachable before the first start. Redis is not a
|
||||
cache the app can do without — it holds the Sidekiq queues, the Rails cache
|
||||
store and the rate limiter.
|
||||
|
||||
What the probes do with that, per container:
|
||||
web liveness and startup use /up, which is Rails' boot check and
|
||||
touches no database — a Postgres outage therefore does not restart
|
||||
the pod. Readiness uses "/", which runs a User.count query, so the
|
||||
pod leaves the Service while the database is gone and returns on
|
||||
its own afterwards.
|
||||
worker all three probes run "sidekiqmon processes" and look for this
|
||||
pod's own entry, i.e. a live Sidekiq heartbeat in Redis. There is
|
||||
no HTTP and no dependency-free alternative: with Redis gone the
|
||||
worker has nothing to do, and liveness only restarts after
|
||||
{{ $workerLiveness.failureThreshold }} x {{ $workerLiveness.periodSeconds }}s = {{ mul $workerLiveness.failureThreshold $workerLiveness.periodSeconds }}s of missing heartbeat.
|
||||
|
||||
A pod that never turns Ready is usually a connection problem, not a chart
|
||||
problem: read the container logs, not the pod events.
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} logs statefulset/{{ .Release.Name }}-{{ .Chart.Name }}-sfs -c {{ .Release.Name }}-{{ .Chart.Name }}-web
|
||||
|
||||
THE FIRST START MIGRATES, AND THE STARTUP PROBE IS THE CLOCK
|
||||
The image entrypoint runs "./bin/rails db:prepare" — but only when the
|
||||
command is "./bin/rails server". That is the web container, and only the web
|
||||
container: the worker runs "bundle exec sidekiq" and never migrates, so the
|
||||
two can never race. Puma binds the port after the migration, which means the
|
||||
whole schema creation happens inside the startup probe's budget:
|
||||
|
||||
web {{ $webStartup.failureThreshold }} x {{ $webStartup.periodSeconds }}s = {{ mul $webStartup.failureThreshold $webStartup.periodSeconds }}s
|
||||
worker {{ $workerStartup.failureThreshold }} x {{ $workerStartup.periodSeconds }}s = {{ mul $workerStartup.failureThreshold $workerStartup.periodSeconds }}s (it waits for the schema the web container creates)
|
||||
|
||||
Raise sure.web.startupProbe.failureThreshold BEFORE a large app-version jump
|
||||
or a first import, not after the crash loop has started — a restart lands in
|
||||
the middle of a migration. Back up the database before any app-version bump:
|
||||
migrations are one-way, and a helm rollback returns the old image to a
|
||||
database the new one has already rewritten.
|
||||
|
||||
ONE POD, ONE VOLUME, TWO CONTAINERS
|
||||
Uploads, imports and exports live on the ReadWriteOnce PVC
|
||||
{{ .Release.Name }}-{{ .Chart.Name }}-pvc, mounted at /rails/storage in both containers. That is
|
||||
the reason web and worker share a pod at all: a background job writes the
|
||||
same files the web process serves, and one RWO volume cannot be attached to
|
||||
two pods.
|
||||
{{- if gt (int $replicas) 1 }}
|
||||
|
||||
sure.replicas is {{ $replicas }}. Expect the extra pods to stay Pending on
|
||||
"Multi-Attach error for volume" unless the storage class is genuinely RWX.
|
||||
{{- end }}
|
||||
|
||||
Backup is two things at the same time: the database holds the accounts and
|
||||
one row per attachment, the PVC holds the bytes. Restoring one without the
|
||||
other gives an app full of broken references. Because the PVC is rendered by
|
||||
the chart, helm uninstall deletes it — the external database survives that,
|
||||
which makes an accidental uninstall look recoverable while the files are
|
||||
already gone.
|
||||
|
||||
THE CHART CONFIGURES NO MAIL AND NO AI
|
||||
Sure sends invitations and notifications over SMTP and can run an assistant
|
||||
against an OpenAI-compatible endpoint. This chart sets neither: no SMTP_*
|
||||
variable, no OPENAI_* variable. Mails are therefore quietly never sent, and
|
||||
the AI features stay off — nothing in the UI says so.
|
||||
|
||||
Both are plain environment variables, so they go into sure.web.env and, for
|
||||
anything a background job needs (the assistant runs in the worker), into
|
||||
sure.worker.env as well. The secretKeyRef form is available for the API key
|
||||
and the SMTP password. Note that upstream sizes AI usage at 1 GB of memory
|
||||
per process; the chart's computed memory limit is 3x its request.
|
||||
{{- if or .Values.sure.web.env .Values.sure.worker.env }}
|
||||
|
||||
Currently set: {{ len .Values.sure.web.env }} extra variable(s) on the web container,
|
||||
{{ len .Values.sure.worker.env }} on the worker, rendered after the ones the chart sets itself.
|
||||
An entry repeating a name the chart already sets wins at install time and is
|
||||
silently lost on the next helm upgrade — read it back off the StatefulSet.
|
||||
{{- end }}
|
||||
@@ -1,451 +0,0 @@
|
||||
{{/*
|
||||
Render the body of a container "resources:" block from a values dict of the form
|
||||
|
||||
resources:
|
||||
limitFactor: 3 # optional, default 3
|
||||
requests: { cpu, memory, ephemeral-storage / ephemeralStorage, ... }
|
||||
limits: { ... } # optional, every entry optional
|
||||
|
||||
Rules (variant B, see issue #19):
|
||||
- requests are rendered as given (camelCase "ephemeralStorage" is normalized
|
||||
to the Kubernetes key "ephemeral-storage").
|
||||
- an explicitly set limit always wins.
|
||||
- a missing memory / ephemeral-storage limit is computed as
|
||||
limitFactor x the corresponding request.
|
||||
- a missing cpu limit is NOT set at all (the kubelet then treats CPU as
|
||||
unlimited; CPU is compressible, so this cannot kill the workload).
|
||||
|
||||
NOTE: this helper is copied into every chart's templates/_helpers.tpl with the
|
||||
chart's name as define prefix rather than shared through a library chart; the
|
||||
reference implementation is template-chart/templates/_helpers.tpl.
|
||||
*/}}
|
||||
{{- define "sure.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
{{- $factor := 3.0 -}}
|
||||
{{- if not (kindIs "invalid" $resources.limitFactor) -}}
|
||||
{{- $factor = float64 $resources.limitFactor -}}
|
||||
{{- end -}}
|
||||
{{- $requests := dict -}}
|
||||
{{- range $key, $value := ($resources.requests | default dict) -}}
|
||||
{{- if not (kindIs "invalid" $value) -}}
|
||||
{{- $_ := set $requests (include "sure.resourceKey" $key) $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $limits := dict -}}
|
||||
{{- range $key, $value := ($resources.limits | default dict) -}}
|
||||
{{- if not (kindIs "invalid" $value) -}}
|
||||
{{- $_ := set $limits (include "sure.resourceKey" $key) $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- range $key, $request := $requests -}}
|
||||
{{- if and (ne $key "cpu") (not (hasKey $limits $key)) -}}
|
||||
{{- $_ := set $limits $key (include "sure.multiplyQuantity" (dict "quantity" $request "factor" $factor)) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $out := dict -}}
|
||||
{{- with $requests }}{{- $_ := set $out "requests" . }}{{- end -}}
|
||||
{{- with $limits }}{{- $_ := set $out "limits" . }}{{- end -}}
|
||||
{{- toYaml $out -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Normalize a resource name from values to its Kubernetes key.
|
||||
*/}}
|
||||
{{- define "sure.resourceKey" -}}
|
||||
{{- if eq . "ephemeralStorage" -}}ephemeral-storage{{- else -}}{{ . }}{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Multiply a Kubernetes quantity by a factor, keeping its suffix.
|
||||
Input: dict "quantity" <string|number> "factor" <float64>.
|
||||
Handles plain numbers ("2", "0.5", 1073741824), decimal ("1.5Gi"), binary
|
||||
("512Mi", "1Gi", "10Ki") and milli/SI suffixes ("100m", "500M"). Integer
|
||||
results are printed without a decimal point.
|
||||
*/}}
|
||||
{{- define "sure.multiplyQuantity" -}}
|
||||
{{- $quantity := include "sure.formatNumber" .quantity -}}
|
||||
{{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}}
|
||||
{{- if not $number -}}
|
||||
{{- fail (printf "cannot parse resource quantity %q" $quantity) -}}
|
||||
{{- end -}}
|
||||
{{- $suffix := trimPrefix $number $quantity -}}
|
||||
{{- if not (regexMatch "^(m|k|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$" $suffix) -}}
|
||||
{{- fail (printf "unsupported resource quantity suffix %q in %q" $suffix $quantity) -}}
|
||||
{{- end -}}
|
||||
{{- $product := mulf (float64 $number) .factor -}}
|
||||
{{- if eq $product (floor $product) -}}
|
||||
{{- printf "%d%s" (int64 $product) $suffix -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s%s" (include "sure.formatNumber" $product) $suffix -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render a number as a plain decimal string, never in exponential notation.
|
||||
|
||||
Why this exists (issue #157): the null-safe fallback helpers pass values
|
||||
through fromYaml, which parses every number as float64. Go's "%v" switches
|
||||
float64 to exponential notation from about 1e6 upwards, so a bare byte count
|
||||
like 1048576 became "1.048576e+06" - not a valid Kubernetes quantity, and the
|
||||
render aborted. Strings ("512Mi") are passed through untouched.
|
||||
*/}}
|
||||
{{- define "sure.formatNumber" -}}
|
||||
{{- if kindIs "float64" . -}}
|
||||
{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%v" . -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render the replica count for the StatefulSet.
|
||||
|
||||
Input: dict
|
||||
"scaleDown" the chart-global .Values.scaleDown flag (may be nil)
|
||||
"replicas" the workload's replicas value (may be nil)
|
||||
|
||||
Rules (issue #25):
|
||||
- scaleDown: true takes precedence and always yields 0.
|
||||
- otherwise an explicitly set replicas value is used as given, including an
|
||||
explicit 0 - Helm's "default" would swallow 0, hence the explicit
|
||||
kindIs "invalid" nil check.
|
||||
- a missing replicas value defaults to 1.
|
||||
*/}}
|
||||
{{- define "sure.replicas" -}}
|
||||
{{- if .scaleDown -}}
|
||||
0
|
||||
{{- else if not (kindIs "invalid" .replicas) -}}
|
||||
{{- .replicas -}}
|
||||
{{- else -}}
|
||||
1
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Merge a user-supplied values sub-tree onto a dict of chart defaults so that a
|
||||
null / empty override falls back to the defaults instead of erasing them.
|
||||
|
||||
Input: dict "defaults" <dict> "given" <dict|nil> "path" <string, optional
|
||||
values path used in error messages>. Output: YAML of the merged dict (consume
|
||||
it with fromYaml).
|
||||
|
||||
Why this exists (issues #61 and #99): Helm merges values maps recursively, but
|
||||
an override key written without children - e.g. a leftover
|
||||
|
||||
securityContext:
|
||||
|
||||
after deleting its last remaining sub-key - is YAML null. Where the chart
|
||||
defines a default, Helm treats that null as a deletion of the default; the
|
||||
template then renders "securityContext: null": no readOnlyRootFilesystem, no
|
||||
dropped capabilities, no allowPrivilegeEscalation: false. The workload starts
|
||||
and reports healthy while being unhardened, so nothing points at the mistake.
|
||||
The same shape erases a probe (it disappears) or the resources block (no
|
||||
requests, no computed limits). This helper defines the opposite semantics: an
|
||||
empty block means "chart defaults apply".
|
||||
*/}}
|
||||
{{- define "sure.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
{{- $given := .given -}}
|
||||
{{- $path := .path | default "value" -}}
|
||||
{{- if kindIs "invalid" $given -}}
|
||||
{{- $given = dict -}}
|
||||
{{- end -}}
|
||||
{{- if not (kindIs "map" $given) -}}
|
||||
{{- fail (printf "%s must be a mapping or null, got %s (%v)" $path (kindOf $given) $given) -}}
|
||||
{{- end -}}
|
||||
{{- $out := deepCopy $defaults -}}
|
||||
{{- range $key, $value := $given -}}
|
||||
{{- if not (kindIs "invalid" $value) -}}
|
||||
{{- $default := index $defaults $key -}}
|
||||
{{- if and (kindIs "map" $value) (kindIs "map" $default) -}}
|
||||
{{- $_ := set $out $key (fromYaml (include "sure.defaultedDict" (dict "defaults" $default "given" $value "path" (printf "%s.%s" $path $key)))) -}}
|
||||
{{- else if kindIs "map" $default -}}
|
||||
{{- fail (printf "%s.%s must be a mapping or null, got %s (%v)" $path $key (kindOf $value) $value) -}}
|
||||
{{- else -}}
|
||||
{{- $_ := set $out $key $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- toYaml $out -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Pick one sub-block out of a values block that may itself be null.
|
||||
|
||||
Input: dict "block" <dict|nil> "key" <string> "path" <string>.
|
||||
Returns YAML of dict "value" <the sub-block, may be nil>, after rejecting a
|
||||
non-mapping block with a message that names the values path. Needed because
|
||||
".Values.sure.web.securityContext" can itself be erased, and indexing into nil
|
||||
inside a template is not an error but silently yields nothing.
|
||||
*/}}
|
||||
{{- define "sure.subBlock" -}}
|
||||
{{- $block := .block -}}
|
||||
{{- if kindIs "invalid" $block -}}
|
||||
{{- $block = dict -}}
|
||||
{{- end -}}
|
||||
{{- if not (kindIs "map" $block) -}}
|
||||
{{- fail (printf "%s must be a mapping or null, got %s (%v)" .path (kindOf $block) $block) -}}
|
||||
{{- end -}}
|
||||
{{- toYaml (dict "value" (index $block .key)) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The effective security contexts, probes and resources (issue #99).
|
||||
|
||||
The defaults below MUST stay in sync with the corresponding blocks in
|
||||
values.yaml, which stays the documented, user-facing place for them; this copy
|
||||
is only the fallback for the case where an override has erased the block. Every
|
||||
reason behind a value is written down there, including what each probe actually
|
||||
proves - a probe default without its path, port name or exec command would
|
||||
leave the workload with no working probe at all (README, guideline 12.5).
|
||||
*/}}
|
||||
{{- define "sure.podSecurityContext" -}}
|
||||
{{- $given := (fromYaml (include "sure.subBlock" (dict "block" . "key" "pod" "path" "sure.securityContext"))).value -}}
|
||||
{{- $defaults := dict
|
||||
"runAsNonRoot" true
|
||||
"runAsUser" 1000
|
||||
"runAsGroup" 1000
|
||||
"fsGroup" 1000
|
||||
"fsGroupChangePolicy" "Always"
|
||||
"seccompProfile" (dict "type" "RuntimeDefault") -}}
|
||||
{{- include "sure.defaultedDict" (dict "defaults" $defaults "given" $given "path" "sure.securityContext.pod") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "sure.containerSecurityContext" -}}
|
||||
{{- $given := (fromYaml (include "sure.subBlock" (dict "block" .block "key" "container" "path" .path))).value -}}
|
||||
{{- $defaults := dict
|
||||
"allowPrivilegeEscalation" false
|
||||
"readOnlyRootFilesystem" true
|
||||
"runAsNonRoot" true
|
||||
"capabilities" (dict "drop" (list "ALL")) -}}
|
||||
{{- include "sure.defaultedDict" (dict "defaults" $defaults "given" $given "path" (printf "%s.container" .path)) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Web container probes. /up is Rails' boot check and touches neither Postgres nor
|
||||
Redis, so liveness and startup use it; readiness uses "/", which runs
|
||||
User.count through authenticate_user! in self-hosted mode and therefore fails
|
||||
while the database is unreachable. The long startup budget covers the
|
||||
"./bin/rails db:prepare" the entrypoint runs before Puma binds the port.
|
||||
*/}}
|
||||
{{- define "sure.webLivenessProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (dict "path" "/up" "port" "http")
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 3 -}}
|
||||
{{- include "sure.defaultedDict" (dict "defaults" $defaults "given" . "path" "sure.web.livenessProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "sure.webReadinessCommand" -}}
|
||||
{{- list "/bin/bash" "-c" "code=\"$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:3000/)\"; test \"$code\" -ge 200 -a \"$code\" -lt 400" | toJson -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "sure.webReadinessProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"exec" (dict "command" (include "sure.webReadinessCommand" . | fromJsonArray))
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 10
|
||||
"failureThreshold" 3 -}}
|
||||
{{- include "sure.defaultedDict" (dict "defaults" $defaults "given" . "path" "sure.web.readinessProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "sure.webStartupProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (dict "path" "/up" "port" "http")
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 60 -}}
|
||||
{{- include "sure.defaultedDict" (dict "defaults" $defaults "given" . "path" "sure.web.startupProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Worker container probes. Sidekiq serves no HTTP, so all three ask its own
|
||||
monitor whether THIS process still holds a heartbeat in Redis; the exec command
|
||||
is part of the default, not decoration (README, guideline 12.5).
|
||||
*/}}
|
||||
{{- define "sure.workerProbeCommand" -}}
|
||||
{{- list "/bin/bash" "-c" "bundle exec sidekiqmon processes | grep -q \"^${HOSTNAME}:\"" | toJson -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "sure.workerLivenessProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"exec" (dict "command" (include "sure.workerProbeCommand" . | fromJsonArray))
|
||||
"periodSeconds" 60
|
||||
"timeoutSeconds" 15
|
||||
"failureThreshold" 5 -}}
|
||||
{{- include "sure.defaultedDict" (dict "defaults" $defaults "given" . "path" "sure.worker.livenessProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "sure.workerReadinessProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"exec" (dict "command" (include "sure.workerProbeCommand" . | fromJsonArray))
|
||||
"periodSeconds" 30
|
||||
"timeoutSeconds" 15
|
||||
"failureThreshold" 3 -}}
|
||||
{{- include "sure.defaultedDict" (dict "defaults" $defaults "given" . "path" "sure.worker.readinessProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "sure.workerStartupProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"exec" (dict "command" (include "sure.workerProbeCommand" . | fromJsonArray))
|
||||
"periodSeconds" 20
|
||||
"timeoutSeconds" 15
|
||||
"failureThreshold" 30 -}}
|
||||
{{- include "sure.defaultedDict" (dict "defaults" $defaults "given" . "path" "sure.worker.startupProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The effective resources blocks, fed into the resources helper above so that an
|
||||
erased "resources:" still yields the chart's requests and computed limits
|
||||
instead of a container without any resource accounting. Both containers run the
|
||||
same image and carry the same request; see values.yaml for where the number
|
||||
comes from.
|
||||
*/}}
|
||||
{{- define "sure.webResources" -}}
|
||||
{{- $defaults := dict
|
||||
"limitFactor" 3
|
||||
"requests" (dict "cpu" 0.1 "memory" "512Mi" "ephemeral-storage" "256Mi") -}}
|
||||
{{- $merged := fromYaml (include "sure.defaultedDict" (dict "defaults" $defaults "given" . "path" "sure.web.resources")) -}}
|
||||
{{- include "sure.resources" $merged -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "sure.workerResources" -}}
|
||||
{{- $defaults := dict
|
||||
"limitFactor" 3
|
||||
"requests" (dict "cpu" 0.1 "memory" "512Mi" "ephemeral-storage" "256Mi") -}}
|
||||
{{- $merged := fromYaml (include "sure.defaultedDict" (dict "defaults" $defaults "given" . "path" "sure.worker.resources")) -}}
|
||||
{{- include "sure.resources" $merged -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The environment both containers share: the same image, the same database, the
|
||||
same Redis, the same secrets. Only the command differs.
|
||||
|
||||
Input: the root context.
|
||||
|
||||
ingress.domain is read HERE, outside the Ingress template, three times over
|
||||
(APP_DOMAIN, WEBAUTHN_RP_ID, WEBAUTHN_ALLOWED_ORIGINS), so it carries its own
|
||||
required() and stays mandatory when ingress.enabled is false - README,
|
||||
guideline 12.1. An empty value would not fail the render: it would register
|
||||
passkeys against an empty relying party and build mail links without a host.
|
||||
|
||||
SELF_HOSTED=true is not optional for this chart: it is what turns Sure into the
|
||||
single-family, no-subscription mode, and it is also what makes the readiness
|
||||
probe on "/" reach the database at all (see values.yaml).
|
||||
*/}}
|
||||
{{- define "sure.commonEnv" -}}
|
||||
{{- $domain := required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain -}}
|
||||
- name: SELF_HOSTED
|
||||
value: "true"
|
||||
- name: RAILS_ENV
|
||||
value: "production"
|
||||
# Not cosmetic and not optional: /tmp is an emptyDir, which Kubernetes creates
|
||||
# mode 0777 without a sticky bit, and Ruby skips exactly that shape when it
|
||||
# looks for a temporary directory. Without this the app aborts during boot with
|
||||
# "could not find a temporary directory". The directory is created by the
|
||||
# init container in the StatefulSet, which explains the whole case.
|
||||
- name: TMPDIR
|
||||
value: "/tmp/ruby"
|
||||
# The image's home directory (/home/rails) is part of the read-only root
|
||||
# filesystem, and Bundler reacts to that by creating a throwaway home under
|
||||
# TMPDIR on EVERY invocation ("`/home/rails` is not writable. Bundler will use
|
||||
# `/tmp/ruby/bundler2026...` as your home directory temporarily", measured in
|
||||
# the install-test). That is one directory per `bundle exec` - including every
|
||||
# worker probe, every 30 seconds. Pointing HOME at a writable directory turns
|
||||
# that into one stable path.
|
||||
- name: HOME
|
||||
value: "/tmp/home"
|
||||
- name: PORT
|
||||
value: "3000"
|
||||
# Sure's production.rb defaults BOTH of these to true. The chart renders them
|
||||
# explicitly from ingress.enabled instead: with the Ingress terminating TLS,
|
||||
# assume_ssl makes Rails treat the forwarded request as secure (so force_ssl
|
||||
# adds HSTS and secure cookies without redirecting), while without an Ingress
|
||||
# both have to be off - a port-forwarded plain-HTTP session would otherwise
|
||||
# either be redirected to https or hand out cookies the browser never returns.
|
||||
- name: RAILS_ASSUME_SSL
|
||||
value: "{{ .Values.ingress.enabled }}"
|
||||
- name: RAILS_FORCE_SSL
|
||||
value: "{{ .Values.ingress.enabled }}"
|
||||
# Action Mailer builds absolute URLs from this host, and it is the fallback the
|
||||
# WebAuthn initializer uses when WEBAUTHN_RP_ID is unset.
|
||||
- name: APP_DOMAIN
|
||||
value: {{ $domain | quote }}
|
||||
# Passkeys are bound to the relying party id; changing it later invalidates
|
||||
# every registered credential (upstream docs/hosting/webauthn.md).
|
||||
- name: WEBAUTHN_RP_ID
|
||||
value: {{ $domain | quote }}
|
||||
- name: WEBAUTHN_ALLOWED_ORIGINS
|
||||
value: {{ printf "https://%s" $domain | quote }}
|
||||
- name: SECRET_KEY_BASE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
||||
key: secret-key-base
|
||||
- name: DB_HOST
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
||||
key: host
|
||||
- name: DB_PORT
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
||||
key: port
|
||||
- name: POSTGRES_DB
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
||||
key: database
|
||||
- name: POSTGRES_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
||||
key: username
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
||||
key: password
|
||||
- name: REDIS_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-redis-secret
|
||||
key: connection-url
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The additive env block of one container (README, guideline 10).
|
||||
|
||||
Input: dict "root" <root context> "env" <the container's env list>.
|
||||
Entries are rendered through tpl, so a value may contain Helm templating, and
|
||||
the value, secretKeyRef and configMapKeyRef forms are supported.
|
||||
*/}}
|
||||
{{- define "sure.env" -}}
|
||||
{{- $root := .root -}}
|
||||
{{- range $value := .env }}
|
||||
{{- if $value.value }}
|
||||
- name: {{ $value.name | quote }}
|
||||
# quote is required, not cosmetic: a value like "3" or "true" renders
|
||||
# unquoted as a YAML number/bool, and the API server then rejects the whole
|
||||
# manifest with "cannot unmarshal number into Go struct field EnvVar.value of
|
||||
# type string".
|
||||
value: {{ tpl $value.value $root | quote }}
|
||||
{{- else if $value.valueFrom }}
|
||||
{{- if $value.valueFrom.secretKeyRef }}
|
||||
- name: {{ $value.name }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ tpl $value.valueFrom.secretKeyRef.name $root | quote }}
|
||||
key: {{ tpl $value.valueFrom.secretKeyRef.key $root | quote }}
|
||||
{{- else if $value.valueFrom.configMapKeyRef }}
|
||||
- name: {{ $value.name }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ tpl $value.valueFrom.configMapKeyRef.name $root | quote }}
|
||||
key: {{ tpl $value.valueFrom.configMapKeyRef.key $root | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
||||
spec:
|
||||
itemPath: {{ required "A Secret-Reference for the database must be provided (secrets.dbSecretRef)" .Values.secrets.dbSecretRef }}
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-redis-secret
|
||||
spec:
|
||||
itemPath: {{ required "A Secret-Reference for Redis must be provided (secrets.redisSecretRef)" .Values.secrets.redisSecretRef }}
|
||||
@@ -1,28 +0,0 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided" .Values.ingress.clusterIssuer }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
rules:
|
||||
- host: {{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
port:
|
||||
name: http
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}
|
||||
secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
{{- end }}
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.storage.amount }}
|
||||
{{- if .Values.storage.storageClass }}
|
||||
storageClassName: {{ .Values.storage.storageClass }}
|
||||
{{- end }}
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
||||
spec:
|
||||
itemPath: {{ required "A Secret-Reference for the app secret must be provided (secrets.appSecretRef)" .Values.secrets.appSecretRef }}
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
spec:
|
||||
selector:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: http
|
||||
name: http
|
||||
type: ClusterIP
|
||||
@@ -1,231 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
spec:
|
||||
serviceName: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
replicas: {{ include "sure.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.sure.replicas) }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
template:
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
annotations:
|
||||
# The chart has no ConfigMap, so there is no checksum/config here. What
|
||||
# these annotations cover is the SOURCE of the secrets, not their
|
||||
# content: the Kubernetes Secrets are materialized by the 1Password
|
||||
# operator, so Helm never sees what is in them. Repointing one of the
|
||||
# secrets.*SecretRef values at a different item therefore changes the
|
||||
# pod template and rolls the pod, instead of silently leaving the old
|
||||
# credentials in the running container. A change of an item's CONTENT
|
||||
# inside the vault is not covered - see the comment on secrets in
|
||||
# values.yaml for why that gap cannot be closed here.
|
||||
# The name differs from checksum/config on purpose - that one promises
|
||||
# "the config changed", this one only "the config source changed".
|
||||
checksum/secret-ref: {{ include (print $.Template.BasePath "/sure.secret.yaml") . | sha256sum }}
|
||||
checksum/secret-ref-db: {{ include (print $.Template.BasePath "/sure-db.secret.yaml") . | sha256sum }}
|
||||
checksum/secret-ref-redis: {{ include (print $.Template.BasePath "/sure-redis.secret.yaml") . | sha256sum }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
{{- include "sure.podSecurityContext" .Values.sure.securityContext | nindent 8 }}
|
||||
initContainers:
|
||||
# Ruby refuses a world-writable directory that has no sticky bit as its
|
||||
# temporary directory (ruby 3.4, tmpdir.rb: a candidate is skipped when
|
||||
# "world_writable? && !sticky?"), and a Kubernetes emptyDir is created
|
||||
# mode 0777 WITHOUT the sticky bit. With /tmp mounted as an emptyDir and
|
||||
# the image's root filesystem read-only, Ruby then finds no candidate at
|
||||
# all and both containers die during boot - measured in this chart's
|
||||
# first install-test, which logged
|
||||
#
|
||||
# system temporary path is world-writable: /tmp
|
||||
# /tmp is world-writable: /tmp
|
||||
# . is not writable: /rails
|
||||
# ArgumentError: could not find a temporary directory
|
||||
#
|
||||
# raised from MiniMagick, which Active Storage loads while Rails boots.
|
||||
# Note where it does NOT come from: nothing in the app asked for a temp
|
||||
# file yet.
|
||||
#
|
||||
# The fix is a directory that is writable without being world-writable.
|
||||
# This init container creates two of them per app container inside the
|
||||
# same emptyDirs (mode 0700, owned by the pod's uid): "ruby" for TMPDIR,
|
||||
# and "home" for HOME - the image's /home/rails is read-only, and
|
||||
# Bundler answers that by creating a throwaway home under TMPDIR on
|
||||
# every single invocation. It runs as the same non-root uid as the app
|
||||
# and needs no capability at all: the emptyDir root is 0777, so creating an
|
||||
# entry in it is allowed, and chmod on a directory one has just created
|
||||
# needs no privilege. That is deliberately weaker than the chown-as-root
|
||||
# init container paperless-ngx needs.
|
||||
#
|
||||
# The image is the app's own, so there is no second image to pin, no
|
||||
# Renovate block to maintain for it and nothing extra to pull.
|
||||
- name: {{ .Release.Name }}-{{ .Chart.Name }}-init-tmp
|
||||
image: "ghcr.io/we-promise/sure:{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
# command, not args: the image entrypoint would start the application.
|
||||
command:
|
||||
- /bin/bash
|
||||
- -ec
|
||||
- |
|
||||
for dir in /web-tmp/ruby /web-tmp/home /worker-tmp/ruby /worker-tmp/home; do
|
||||
mkdir -p "$dir"
|
||||
chmod 700 "$dir"
|
||||
done
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
volumeMounts:
|
||||
- name: web-tmp
|
||||
mountPath: /web-tmp
|
||||
- name: worker-tmp
|
||||
mountPath: /worker-tmp
|
||||
# Fixed, deliberately small: this container runs two mkdirs and two
|
||||
# chmods. Not values-configurable, for the same reason as the init
|
||||
# container in paperless-ngx - there is nothing here to tune.
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
ephemeral-storage: 1Mi
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
ephemeral-storage: 10Mi
|
||||
containers:
|
||||
# The web process. Its command is the image's default
|
||||
# (CMD ["./bin/rails", "server"]), which is exactly what makes the
|
||||
# entrypoint run "./bin/rails db:prepare" first - the migration only
|
||||
# happens for that command, so it happens here and nowhere else.
|
||||
- name: {{ .Release.Name }}-{{ .Chart.Name }}-web
|
||||
image: "ghcr.io/we-promise/sure:{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
{{- include "sure.commonEnv" . | nindent 12 }}
|
||||
{{- include "sure.env" (dict "root" $ "env" .Values.sure.web.env) | nindent 12 }}
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
protocol: TCP
|
||||
name: http
|
||||
# Probes, resources and both security contexts go through the
|
||||
# null-safe helpers (#99): an erased block means "chart defaults
|
||||
# apply" instead of a vanished probe or an unhardened container.
|
||||
# Never wrap these in an "if" - a guard hides the erased case
|
||||
# completely instead of rendering a visible null.
|
||||
livenessProbe:
|
||||
{{- include "sure.webLivenessProbe" .Values.sure.web.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- include "sure.webReadinessProbe" .Values.sure.web.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- include "sure.webStartupProbe" .Values.sure.web.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "sure.webResources" .Values.sure.web.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- include "sure.containerSecurityContext" (dict "block" .Values.sure.web.securityContext "path" "sure.web.securityContext") | nindent 12 }}
|
||||
volumeMounts:
|
||||
# Active Storage's local disk service, i.e. every uploaded file and
|
||||
# import, lives here (config/storage.yml: Rails.root.join("storage")).
|
||||
- name: storage
|
||||
mountPath: /rails/storage
|
||||
# Writable paths, so readOnlyRootFilesystem: true can stay on.
|
||||
# Each container gets its own emptyDirs: nothing has to be shared
|
||||
# between the two processes, and a shared scratch directory would
|
||||
# only create coupling nobody asked for.
|
||||
#
|
||||
# /tmp: Rack writes multipart uploads (CSV imports, attachments)
|
||||
# here before Active Storage ever sees them. The app does not use
|
||||
# this path directly, though - TMPDIR points at the private
|
||||
# subdirectory the init container creates inside it, because Ruby
|
||||
# rejects a world-writable emptyDir as a temporary directory.
|
||||
- name: web-tmp
|
||||
mountPath: /tmp
|
||||
# /rails/tmp: Rails' own scratch directory - the Bootsnap cache,
|
||||
# Puma's pidfile when PIDFILE is set, and the combined CA bundle the
|
||||
# 00_ssl initializer writes when SSL_CA_FILE is given. Note the
|
||||
# trade-off, read off the Dockerfile: the image ships a Bootsnap
|
||||
# cache precompiled at build time under this path, and mounting an
|
||||
# emptyDir over it hides that cache, so every pod start recompiles
|
||||
# it. Boot time cost, not correctness; the alternative is a
|
||||
# writable image layer, which the hardening standard rules out.
|
||||
- name: web-rails-tmp
|
||||
mountPath: /rails/tmp
|
||||
# /rails/log: production logs go to STDOUT (config/environments/
|
||||
# production.rb), so under normal operation nothing is written here
|
||||
# at all. It stays writable because Rails' default logger path is
|
||||
# log/<env>.log and any rake or console path that falls back to it
|
||||
# would otherwise hit a read-only filesystem. Not measured.
|
||||
- name: web-rails-log
|
||||
mountPath: /rails/log
|
||||
# The Sidekiq process. Same image, different command - and because that
|
||||
# command is NOT "./bin/rails server", the image entrypoint skips
|
||||
# db:prepare here, so the two containers never migrate against each
|
||||
# other.
|
||||
#
|
||||
# args, NOT command, and that distinction is load-bearing: Kubernetes'
|
||||
# "command" replaces the image's ENTRYPOINT, which here is
|
||||
# /rails/bin/docker-entrypoint - the script that preloads jemalloc.
|
||||
# Upstream counts jemalloc among the three things that make the
|
||||
# documented memory figures hold, so replacing the entrypoint would
|
||||
# silently give this container a different memory profile than the one
|
||||
# its requests are sized for. "args" replaces only the CMD, which is
|
||||
# what upstream's compose file does too.
|
||||
- name: {{ .Release.Name }}-{{ .Chart.Name }}-worker
|
||||
image: "ghcr.io/we-promise/sure:{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- bundle
|
||||
- exec
|
||||
- sidekiq
|
||||
env:
|
||||
{{- include "sure.commonEnv" . | nindent 12 }}
|
||||
{{- include "sure.env" (dict "root" $ "env" .Values.sure.worker.env) | nindent 12 }}
|
||||
livenessProbe:
|
||||
{{- include "sure.workerLivenessProbe" .Values.sure.worker.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- include "sure.workerReadinessProbe" .Values.sure.worker.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- include "sure.workerStartupProbe" .Values.sure.worker.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "sure.workerResources" .Values.sure.worker.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- include "sure.containerSecurityContext" (dict "block" .Values.sure.worker.securityContext "path" "sure.worker.securityContext") | nindent 12 }}
|
||||
volumeMounts:
|
||||
# The worker reads and writes the same Active Storage files as the
|
||||
# web process (imports are processed as background jobs), which is
|
||||
# why upstream's compose file mounts one volume into both and why
|
||||
# both containers live in one pod here: a single ReadWriteOnce PVC
|
||||
# cannot be attached to two pods.
|
||||
- name: storage
|
||||
mountPath: /rails/storage
|
||||
- name: worker-tmp
|
||||
mountPath: /tmp
|
||||
- name: worker-rails-tmp
|
||||
mountPath: /rails/tmp
|
||||
- name: worker-rails-log
|
||||
mountPath: /rails/log
|
||||
restartPolicy: Always
|
||||
volumes:
|
||||
- name: storage
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Release.Name }}-{{ .Chart.Name }}-pvc
|
||||
- name: web-tmp
|
||||
emptyDir: {}
|
||||
- name: web-rails-tmp
|
||||
emptyDir: {}
|
||||
- name: web-rails-log
|
||||
emptyDir: {}
|
||||
- name: worker-tmp
|
||||
emptyDir: {}
|
||||
- name: worker-rails-tmp
|
||||
emptyDir: {}
|
||||
- name: worker-rails-log
|
||||
emptyDir: {}
|
||||
@@ -1,234 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||
"title": "sure values",
|
||||
"description": "Strict values schema (issue #68): additionalProperties is false on every chart-owned object level, so a key the chart does not read is rejected at render time instead of being silently ignored. The objects that go verbatim into the Kubernetes pod spec (probes, security contexts, global, free-form annotations) stay open, because the chart does not interpret their contents. The hardening, probe and resources blocks additionally accept null - the chart's null-safe helpers then fall back to ITS defaults (issue #99). The schema adds to required(), it does not replace it.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"global": {
|
||||
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
|
||||
"type": "object"
|
||||
},
|
||||
"scaleDown": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"secrets": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"appSecretRef": {
|
||||
"description": "op:// item path of the app secret. Field: secret-key-base (SECRET_KEY_BASE).",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"dbSecretRef": {
|
||||
"description": "op:// item path of the PostgreSQL credentials. Fields: host, port, database, username, password.",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"redisSecretRef": {
|
||||
"description": "op:// item path of the Redis connection. Field: connection-url (REDIS_URL).",
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"sure": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"replicas": {
|
||||
"description": "Replicas of the StatefulSet; both containers scale with it, because they share one ReadWriteOnce PVC.",
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"securityContext": {
|
||||
"description": "Pod-level hardening, shared by both containers. May be null / empty: the chart then falls back to ITS defaults (uid/gid 1000, matching the image's USER 1000:1000) instead of rendering an unhardened pod.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"pod": {
|
||||
"description": "Merged onto the chart's pod hardening defaults and rendered as the pod's securityContext; kept open because the chart does not interpret the individual fields.",
|
||||
"type": ["object", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"securityContext": {
|
||||
"$ref": "#/definitions/containerSecurityContext"
|
||||
},
|
||||
"resources": {
|
||||
"$ref": "#/definitions/resources"
|
||||
},
|
||||
"livenessProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"readinessProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"startupProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"env": {
|
||||
"$ref": "#/definitions/env"
|
||||
}
|
||||
}
|
||||
},
|
||||
"worker": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"securityContext": {
|
||||
"$ref": "#/definitions/containerSecurityContext"
|
||||
},
|
||||
"resources": {
|
||||
"$ref": "#/definitions/resources"
|
||||
},
|
||||
"livenessProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"readinessProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"startupProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"env": {
|
||||
"$ref": "#/definitions/env"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"description": "The Active Storage PVC, mounted at /rails/storage in both containers. Deliberately NOT null-safe: an erased block yields an invalid PVC, which fails loudly.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"storageClass": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ingress": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Gates the Ingress template (issue #93) and decides whether the chart renders RAILS_ASSUME_SSL/RAILS_FORCE_SSL as true or false. It does NOT free ingress.domain: that value is also read for APP_DOMAIN, WEBAUTHN_RP_ID and WEBAUTHN_ALLOWED_ORIGINS and keeps its own required() there (guideline 12.1).",
|
||||
"type": "boolean"
|
||||
},
|
||||
"clusterIssuer": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"domain": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"className": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"annotations": {
|
||||
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open.",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"resources": {
|
||||
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"limitFactor": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"requests": {
|
||||
"$ref": "#/definitions/resourceList"
|
||||
},
|
||||
"limits": {
|
||||
"$ref": "#/definitions/resourceList"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourceList": {
|
||||
"description": "Both spellings of the ephemeral-storage key are accepted because the resources helper normalizes camelCase to the Kubernetes key.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"cpu": {
|
||||
"type": ["string", "number", "null"]
|
||||
},
|
||||
"memory": {
|
||||
"type": ["string", "number", "null"]
|
||||
},
|
||||
"ephemeral-storage": {
|
||||
"type": ["string", "number", "null"]
|
||||
},
|
||||
"ephemeralStorage": {
|
||||
"type": ["string", "number", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueFrom": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"secretKeyRef": {
|
||||
"$ref": "#/definitions/keyRef"
|
||||
},
|
||||
"configMapKeyRef": {
|
||||
"$ref": "#/definitions/keyRef"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"keyRef": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "key"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"containerSecurityContext": {
|
||||
"description": "May be null / empty: the chart then falls back to its container hardening defaults (issue #99) instead of rendering an unhardened container. The pod-level context lives one level up, under sure.securityContext.pod, because it is shared by both containers.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"container": {
|
||||
"description": "Merged onto the chart's container hardening defaults and rendered as the container's securityContext; kept open because the chart does not interpret the individual fields.",
|
||||
"type": ["object", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"probe": {
|
||||
"description": "Merged onto the chart's probe defaults and rendered as a container probe; kept open because the chart does not interpret the individual fields. May be null / empty, which means the defaults apply (issue #99) rather than the probe disappearing. The chart's defaults carry the probe's context - the path for the web container, the exec command for the worker - not just its timings (guideline 12.5).",
|
||||
"type": ["object", "null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
# Scale the whole chart down without uninstalling it: scaleDown: true renders
|
||||
# every Deployment/StatefulSet with replicas: 0 (and suspends CronJobs) and
|
||||
# takes precedence over any per-workload replicas value. PVCs, Secrets and
|
||||
# Services stay in place; scaleDown: false brings the workloads back up.
|
||||
scaleDown: false
|
||||
|
||||
# 1Password item paths (op://<vault>/<item>). The 1Password operator turns each
|
||||
# OnePasswordItem into a Kubernetes Secret whose keys are the FIELD NAMES of the
|
||||
# item; the chart references those field names, so a renamed field is a pod in
|
||||
# CreateContainerConfigError, not a render error. NOTES.txt lists every field.
|
||||
#
|
||||
# Helm never sees the CONTENT of these secrets, so no checksum/config-style
|
||||
# annotation can cover them (README, guideline 9). The pod template does carry a
|
||||
# checksum/secret-ref over the rendered OnePasswordItems, so REPOINTING one of
|
||||
# the *SecretRef values at a different item restarts the pod; editing a value
|
||||
# INSIDE the same item does not, and needs a manual
|
||||
# kubectl rollout restart statefulset/<release>-sure-sfs
|
||||
# The operator's operator.1password.io/auto-restart annotation is deliberately
|
||||
# NOT set: it only ever restarts Deployments, so on this StatefulSet it would
|
||||
# advertise a guarantee that does not exist (README, guideline 9).
|
||||
secrets:
|
||||
# SECRET_KEY_BASE (field: secret-key-base)
|
||||
appSecretRef: null
|
||||
# PostgreSQL connection (fields: host, port, database, username, password)
|
||||
dbSecretRef: null
|
||||
# Redis connection URL (field: connection-url)
|
||||
redisSecretRef: null
|
||||
|
||||
sure:
|
||||
# Replica count for the StatefulSet; 0 is allowed and scales it down. The
|
||||
# chart-global scaleDown flag takes precedence.
|
||||
#
|
||||
# Both containers live in the SAME pod, so this value scales web and worker
|
||||
# together. That is a consequence of the single ReadWriteOnce PVC they share
|
||||
# at /rails/storage (Active Storage): splitting them into two workloads would
|
||||
# need a ReadWriteMany volume. Above 1 the extra pods normally stay Pending on
|
||||
# "Multi-Attach error for volume" - see NOTES.txt.
|
||||
replicas: 1
|
||||
# Pod-level hardening, shared by both containers and by the init container.
|
||||
# The upstream image declares USER 1000:1000 (group and user "rails", created
|
||||
# in the Dockerfile), so the chart needs no deviation from the repo hardening
|
||||
# standard; fsGroup 1000 makes the storage PVC writable for that uid.
|
||||
#
|
||||
# readOnlyRootFilesystem: true holds, and the writable paths are emptyDirs:
|
||||
# /rails/tmp, /rails/log and /tmp, one set per container. /tmp needs one extra
|
||||
# step that is easy to mistake for decoration - a tiny init container creates
|
||||
# two private subdirectories in it, and the containers run with TMPDIR and
|
||||
# HOME pointing there. Ruby refuses a world-writable directory without a
|
||||
# sticky bit as its temporary directory and an emptyDir is exactly that, so
|
||||
# without this the app aborts during boot; HOME follows because the image's
|
||||
# /home/rails is read-only and Bundler otherwise builds a throwaway home per
|
||||
# invocation. The StatefulSet carries the measured evidence for both.
|
||||
securityContext:
|
||||
pod:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
fsGroupChangePolicy: Always
|
||||
# RuntimeDefault applies the container runtime's default seccomp filter,
|
||||
# which blocks the syscalls a normal workload never makes. It sits in the
|
||||
# pod context on purpose: from here it covers every container in the pod
|
||||
# - web and worker alike - including init containers, while a
|
||||
# container-level seccompProfile would have to be repeated for each one
|
||||
# (and silently missed on the next one added). A container that genuinely
|
||||
# needs a different filter can still override it under
|
||||
# <container>.securityContext.container, which wins over this value.
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# web - Puma, the container that also runs the database migrations
|
||||
# ---------------------------------------------------------------------------
|
||||
web:
|
||||
securityContext:
|
||||
container:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
resources:
|
||||
# Limits are optional: memory and ephemeral-storage limits default to
|
||||
# limitFactor x the request, an explicitly set limit always wins, and a
|
||||
# CPU limit is only set when configured explicitly under "limits".
|
||||
#
|
||||
# The request follows upstream's own sizing note (docs/hosting/docker.md
|
||||
# for 0.7.4): steady-state memory "around 352 MB", comfortable under a
|
||||
# 512 MB limit, while "if you enable AI, run at least 1 GB". 512Mi request
|
||||
# x limitFactor 3 = 1536Mi limit, which holds both the everyday figure and
|
||||
# the AI recommendation without reserving the larger number on the node.
|
||||
# Read from upstream's documentation, not measured here.
|
||||
#
|
||||
# ephemeral-storage is NOT the template default of 10Mi, and the reason is
|
||||
# measured: with 10Mi (limit 30Mi) per container the kubelet evicted the
|
||||
# pod within two minutes of every start - "Pod ephemeral local storage
|
||||
# usage exceeds the total limit of containers 60Mi". Both emptyDirs and
|
||||
# the container's stdout log count against that limit, and this image
|
||||
# rebuilds its Bootsnap cache under /rails/tmp on every pod start because
|
||||
# the emptyDir hides the one baked into the image. 256Mi request (768Mi
|
||||
# limit) is headroom for that, not a measurement of it - the steady-state
|
||||
# figure is still unknown.
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 0.1
|
||||
memory: 512Mi
|
||||
ephemeral-storage: 256Mi
|
||||
# PROBES - what each one actually proves (README, guideline 12.3)
|
||||
#
|
||||
# /up is Rails' own health route (config/routes.rb: get "up" =>
|
||||
# "rails/health#show"). READ FROM THE RAILS SOURCE, NOT MEASURED HERE: that
|
||||
# controller renders 200 as soon as the app has booted without raising and
|
||||
# touches NO database and NO Redis. It is a boot check, and the widespread
|
||||
# belief that it tests the database is exactly what guideline 12.3 exists to
|
||||
# catch - upstream's own Helm chart (charts/sure in the app repository) uses
|
||||
# /up for readiness, which is the mistake this chart does not copy.
|
||||
#
|
||||
# Liveness therefore uses /up: a Postgres outage must not restart the pod.
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /up
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
# Readiness asks for "/" instead, because that path provably reaches the
|
||||
# database - and it does so through curl rather than an httpGet probe, for
|
||||
# a reason that was measured rather than guessed (see below).
|
||||
#
|
||||
# Why "/" reaches the database: every request runs ApplicationController's
|
||||
# authenticate_user! before_action (app/controllers/concerns/
|
||||
# authentication.rb); without a session cookie - which a probe never has -
|
||||
# it falls into self_hosted_first_login?, i.e.
|
||||
# "config.app_mode.self_hosted? && User.count.zero?". User.count is an SQL
|
||||
# query. Measured in the install-test: against an empty database the app
|
||||
# answered 302 to /registration/new, which is the branch that query decides,
|
||||
# so the path demonstrably touches Postgres. With the database down the
|
||||
# query raises and Rails answers 500.
|
||||
#
|
||||
# Why exec + curl and not httpGet: the kubelet's HTTP prober FOLLOWS
|
||||
# redirects to the same host, and with RAILS_ASSUME_SSL=true (which the
|
||||
# chart sets whenever the Ingress is on) Rails builds that redirect as
|
||||
# https://<pod-ip>:3000/... - a scheme Puma does not speak. The install-test
|
||||
# failed with "http: server gave HTTP response to HTTPS client" on a pod
|
||||
# that was answering correctly. curl without -L asks once and reports the
|
||||
# status code; the test accepts 2xx and 3xx exactly as the kubelet would,
|
||||
# and a 500 from a missing database still fails. curl is part of the image
|
||||
# (the Dockerfile installs it), so nothing extra is pulled in for this.
|
||||
#
|
||||
# One condition this depends on: SELF_HOSTED=true, which the chart always
|
||||
# sets. In managed mode the same code short-circuits before User.count and
|
||||
# "/" would redirect without touching Postgres at all.
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- "code=\"$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:3000/)\"; test \"$code\" -ge 200 -a \"$code\" -lt 400"
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 10
|
||||
failureThreshold: 3
|
||||
# The startup probe gates liveness and readiness, so neither needs an
|
||||
# initialDelaySeconds. Its budget has to cover the first start, which is the
|
||||
# expensive one: the image entrypoint (/rails/bin/docker-entrypoint) runs
|
||||
# "./bin/rails db:prepare" whenever the command is "./bin/rails server", so
|
||||
# the web container creates and migrates the schema before Puma binds the
|
||||
# port. Budget 10s x 60 = 600s. Raise it before a large app-version jump;
|
||||
# a restart in the middle of a migration is the failure mode it prevents.
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /up
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 60
|
||||
# Extra environment variables, appended to the ones the chart always sets.
|
||||
# Values are rendered through tpl; value, secretKeyRef and configMapKeyRef
|
||||
# forms are supported.
|
||||
#
|
||||
# This is also where the AI features go - the chart models none of them as
|
||||
# values on purpose. OPENAI_ACCESS_TOKEN (secretKeyRef form), OPENAI_MODEL,
|
||||
# OPENAI_URI_BASE, LLM_CONTEXT_WINDOW, OPENAI_REQUEST_TIMEOUT,
|
||||
# AI_RESPONSE_TIMEOUT, ASSISTANT_MAX_TOOL_CALL_ITERATIONS and the SMTP_*
|
||||
# block are plain environment variables to this image. Set them on BOTH
|
||||
# containers where a background job needs them as well (the assistant runs
|
||||
# in the worker).
|
||||
#
|
||||
# Additive means additive: an entry repeating a name the chart already sets
|
||||
# reaches the pod twice at install time (the later one, yours, wins) but is
|
||||
# LOST on a later helm upgrade, because Kubernetes merges env by name and
|
||||
# the pair collapses to the chart's own value - with a green upgrade either
|
||||
# way (README, guideline 10).
|
||||
env: []
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# worker - Sidekiq: the same image, started with "bundle exec sidekiq" as
|
||||
# its args (the image entrypoint stays in place; see the StatefulSet)
|
||||
# ---------------------------------------------------------------------------
|
||||
worker:
|
||||
securityContext:
|
||||
container:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
resources:
|
||||
# Same sizing as the web container, for a documented reason: upstream
|
||||
# notes that the sample/demo-data generator runs in the WORKER and is the
|
||||
# one operation that deterministically exceeds 512 MB (observed flat at
|
||||
# ~680 MB on a 2 GB container). The computed limit of 3 x 512Mi = 1536Mi
|
||||
# covers that; the request stays at the everyday figure.
|
||||
#
|
||||
# ephemeral-storage: same 256Mi as the web container and for the same
|
||||
# measured reason - see the comment there.
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 0.1
|
||||
memory: 512Mi
|
||||
ephemeral-storage: 256Mi
|
||||
# PROBES - the worker speaks no HTTP at all, so all three are exec probes.
|
||||
#
|
||||
# The command asks Sidekiq's own monitor whether THIS process is in the
|
||||
# process set: sidekiqmon prints one line per registered process, each
|
||||
# starting with the identity "<hostname>:<pid>:<nonce>" (sidekiq 8.0.5,
|
||||
# lib/sidekiq/component.rb), and in a pod the hostname is the pod name that
|
||||
# bash exposes as $HOSTNAME. Sidekiq keeps that registration alive with a
|
||||
# heartbeat whose Redis key expires after about 60s, so a process that hangs
|
||||
# disappears from the set while its PID still exists - which is precisely
|
||||
# what a process check like "pgrep sidekiq" could not tell you (and would
|
||||
# not tell you anything at all: Sidekiq is PID 1 here, so if it dies the
|
||||
# container dies with it and the check can never fail - a check a broken
|
||||
# system passes, README).
|
||||
#
|
||||
# READ FROM THE SIDEKIQ 8.0.5 SOURCE, NOT MEASURED: sidekiqmon is a plain
|
||||
# Ruby entry point that requires sidekiq/api only - it does NOT boot Rails -
|
||||
# and it reads REDIS_URL from the environment, which the chart sets on this
|
||||
# container. Cost per check is therefore one Ruby start, not an application
|
||||
# boot; the periods below are chosen on the assumption that this takes a few
|
||||
# seconds, and the PR install-test is the first place it actually runs.
|
||||
#
|
||||
# Liveness hangs on Redis, which guideline 12.3 allows only where no
|
||||
# dependency-free path proves anything - that is the case here (see above) -
|
||||
# and it is given a deliberately generous budget of 60s x 5 = 300s of
|
||||
# missing heartbeat, so a short Redis blip does not restart the pod.
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- bundle exec sidekiqmon processes | grep -q "^${HOSTNAME}:"
|
||||
periodSeconds: 60
|
||||
timeoutSeconds: 15
|
||||
failureThreshold: 5
|
||||
# Readiness uses the same check with a tighter budget: a worker that is not
|
||||
# registered in Redis is not doing any work, whatever its process state.
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- bundle exec sidekiqmon processes | grep -q "^${HOSTNAME}:"
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 15
|
||||
failureThreshold: 3
|
||||
# The startup probe gates the other two. Sidekiq boots the full Rails app
|
||||
# before it registers, and on a first install it waits for the web container
|
||||
# to finish db:prepare, so the budget matches the web container's: 20s x 30
|
||||
# = 600s.
|
||||
startupProbe:
|
||||
exec:
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- bundle exec sidekiqmon processes | grep -q "^${HOSTNAME}:"
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 15
|
||||
failureThreshold: 30
|
||||
# Extra environment variables for the worker, appended to the ones the chart
|
||||
# always sets. Same rules as sure.web.env above.
|
||||
env: []
|
||||
|
||||
# The Active Storage volume, mounted at /rails/storage in BOTH containers.
|
||||
# Deliberately not null-safe: an erased block yields an invalid PVC, which fails
|
||||
# loudly (README, guideline 12).
|
||||
storage:
|
||||
amount: 10Gi
|
||||
storageClass: null
|
||||
|
||||
ingress:
|
||||
# Gates the whole Ingress template. Note that ingress.domain is read three
|
||||
# more times outside it - APP_DOMAIN (the host Action Mailer builds link URLs
|
||||
# from), WEBAUTHN_RP_ID and WEBAUTHN_ALLOWED_ORIGINS (the relying party a
|
||||
# passkey is bound to) - so it keeps its own required() there and stays
|
||||
# mandatory with the Ingress switched off; only clusterIssuer becomes
|
||||
# unnecessary (README, guideline 12.1). Without that guard the app would come
|
||||
# up with WEBAUTHN_RP_ID="" and register passkeys against the wrong relying
|
||||
# party - rendered cleanly, pod green.
|
||||
#
|
||||
# ingress.enabled also decides how the app is told about TLS: enabled renders
|
||||
# RAILS_ASSUME_SSL=true and RAILS_FORCE_SSL=true (the Ingress terminates TLS,
|
||||
# so Rails must trust that and may set secure cookies and HSTS), disabled
|
||||
# renders both as "false" so a plain-HTTP port-forward can log in at all.
|
||||
enabled: true
|
||||
clusterIssuer: null
|
||||
domain: null
|
||||
className: nginx
|
||||
# Extra annotations for the Ingress; merged with the cert-manager
|
||||
# cluster-issuer annotation the chart always sets.
|
||||
annotations: {}
|
||||
@@ -5,5 +5,5 @@ icon: https://avatars.githubusercontent.com/u/136759148
|
||||
|
||||
type: application
|
||||
|
||||
version: 5.1.3+up6.0.0.beta13.1
|
||||
appVersion: "6.0.0-beta13.1"
|
||||
version: 5.1.0+up6.0.0.beta12.1
|
||||
appVersion: "6.0.0-beta12.1"
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "teamspeak-server.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "teamspeak-server.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -170,9 +168,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "teamspeak-server.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "xxx.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "xxx.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -170,9 +168,9 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14). This file is the reference implementation.
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies. This file is the reference
|
||||
implementation.
|
||||
*/}}
|
||||
{{- define "xxx.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
@@ -4,5 +4,5 @@ description: A Helm chart to deploy urlaubsverwaltung on Kubernetes
|
||||
icon: https://raw.githubusercontent.com/urlaubsverwaltung/urlaubsverwaltung/main/src/main/resources/static/favicons/apple-touch-icon.png
|
||||
type: application
|
||||
|
||||
version: 10.0.1+up6.13.1
|
||||
appVersion: 6.13.1
|
||||
version: 10.0.0+up6.12.1
|
||||
appVersion: 6.12.1
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "urlaubsverwaltung.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "urlaubsverwaltung.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -170,9 +168,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "urlaubsverwaltung.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -1,33 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: valkey
|
||||
description: A Helm chart to deploy a single Valkey instance (cache or queue backend) for one application on Kubernetes
|
||||
icon: https://avatars.githubusercontent.com/u/164458127
|
||||
|
||||
type: application
|
||||
|
||||
# appVersion is the exact image tag, "9.1.2-alpine" - the version AND the base
|
||||
# flavour, so the "+up" suffix carries both with the dash normalized to a dot.
|
||||
#
|
||||
# Why "-alpine" and not the fully numbered "-alpine3.24" (Gitea #46, third plan
|
||||
# comment): neither tag is immutable. Upstream rebuilds the whole tag matrix
|
||||
# periodically, and the build timestamps prove it - "9.1-alpine" and
|
||||
# "9.0.6-alpine3.24" were pushed within minutes of each other on 2026-09-21
|
||||
# although 9.0.6 is the older release line. Only a digest is immutable. So the
|
||||
# choice was never "immutable vs. moving" but between two equally moving tags
|
||||
# with different Renovate behaviour, and "-alpine" is the better one: docker
|
||||
# versioning splits at the first dash, reads 9.1.2 as the version and "alpine"
|
||||
# as the compatibility suffix, so the suffix stays constant forever and the
|
||||
# update path can never freeze. With "-alpine3.24" it would: upstream builds
|
||||
# exactly one Alpine base per Valkey release, so the next base arrives as
|
||||
# "9.1.3-alpine3.25" - a candidate a suffix-bound rule never offers, and
|
||||
# "no update" looks identical to a working rule.
|
||||
#
|
||||
# The price, named rather than discovered: a rebuild of 9.1.2-alpine changes the
|
||||
# running image with no change in this repository and nothing reports it. The
|
||||
# remedy if that ever matters is pinDigests for this one image - see the valkey
|
||||
# block in renovate.json5.
|
||||
version: 1.0.0+up9.1.2.alpine
|
||||
appVersion: "9.1.2-alpine"
|
||||
|
||||
maintainers:
|
||||
- name: jklein
|
||||
@@ -1,160 +0,0 @@
|
||||
{{- $replicas := include "valkey.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.valkey.replicas) -}}
|
||||
{{- $limitBytes := include "valkey.memoryLimitBytes" .Values.valkey.resources -}}
|
||||
{{- $maxmemory := include "valkey.maxmemoryBytes" (dict "resources" .Values.valkey.resources "maxmemory" .Values.valkey.maxmemory "maxmemoryFactor" .Values.valkey.maxmemoryFactor) -}}
|
||||
Valkey {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
||||
|
||||
ACCESS — IN-CLUSTER ONLY
|
||||
{{ .Release.Name }}-{{ .Chart.Name }}-service.{{ .Release.Namespace }}.svc.cluster.local:6379
|
||||
|
||||
There is no Ingress and no value to add one: this is one application's cache
|
||||
or queue backend, and its only authentication is the password. Applications
|
||||
address it with a redis:// URL (Valkey speaks the same protocol, and every
|
||||
client library still calls the scheme redis://):
|
||||
|
||||
redis://:<password>@{{ .Release.Name }}-{{ .Chart.Name }}-service.{{ .Release.Namespace }}.svc.cluster.local:6379/0
|
||||
|
||||
The password belongs in the CONSUMING application's own vault item, where its
|
||||
connection URL lives. Nothing here writes it there.
|
||||
|
||||
Check by hand from inside the namespace, with the password out of the pod's own
|
||||
mounted file so it never reaches your shell history:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} exec {{ .Release.Name }}-{{ .Chart.Name }}-sfs-0 -- /bin/sh /etc/valkey/ping.sh && echo OK
|
||||
|
||||
THE 1PASSWORD ITEM MUST CARRY ONE FIELD, AND ITS NAME IS PART OF THE CHART
|
||||
secrets.authSecretRef points at
|
||||
{{ .Values.secrets.authSecretRef }}
|
||||
The 1Password operator materializes that item as the Kubernetes Secret
|
||||
{{ .Release.Name }}-{{ .Chart.Name }}-secret, with one Secret key per field of the item. This chart
|
||||
reads exactly one field:
|
||||
|
||||
auth-conf a COMPLETE Valkey configuration line, not the bare
|
||||
password:
|
||||
|
||||
requirepass <the password>
|
||||
|
||||
A differently named field produces no such key, and the pod then stays in
|
||||
ContainerCreating with "couldn't find key auth-conf" in its events.
|
||||
|
||||
Why a whole configuration line instead of the password: the file is mounted and
|
||||
"include"d by the configuration, so the password never appears in the process
|
||||
list, in an environment variable, or in anything Helm renders. The cost is that
|
||||
a typo in that line would be a Valkey WITHOUT a password — running, healthy,
|
||||
and reachable from the whole pod network, because the official image compiles
|
||||
protected-mode off. An init container therefore checks the mounted file for a
|
||||
"requirepass " line before the server starts and refuses to let the pod run
|
||||
otherwise. If the pod is stuck in Init:Error or Init:CrashLoopBackOff, read it:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} logs {{ .Release.Name }}-{{ .Chart.Name }}-sfs-0 -c {{ .Release.Name }}-{{ .Chart.Name }}-check-auth
|
||||
|
||||
It never prints the field's contents.
|
||||
|
||||
Watch the materialization with
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} get onepassworditem {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
||||
kubectl -n {{ .Release.Namespace }} get secret {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
||||
|
||||
THIS RELEASE IS CONFIGURED AS A {{ if .Values.persistence.enabled }}{{ if eq .Values.valkey.maxmemoryPolicy "noeviction" }}QUEUE{{ else }}PERSISTENT CACHE{{ end }}{{ else }}{{ if eq .Values.valkey.maxmemoryPolicy "noeviction" }}VOLATILE STORE — PROBABLY BY MISTAKE{{ else }}CACHE{{ end }}{{ end }}
|
||||
persistence.enabled: {{ .Values.persistence.enabled }}
|
||||
valkey.maxmemoryPolicy: {{ .Values.valkey.maxmemoryPolicy }}
|
||||
memory limit (computed): {{ include "valkey.humanBytes" $limitBytes }} ({{ $limitBytes }} bytes)
|
||||
maxmemory: {{ include "valkey.humanBytes" $maxmemory }} ({{ $maxmemory }} bytes){{ if kindIs "invalid" .Values.valkey.maxmemory }}, derived as maxmemoryFactor {{ .Values.valkey.maxmemoryFactor | default 0.75 }} x the limit{{ else }}, set explicitly in values and checked against the limit{{ end }}
|
||||
{{ if .Values.persistence.enabled }}
|
||||
{{- if eq .Values.valkey.maxmemoryPolicy "noeviction" }}
|
||||
The combination this chart is built for. When memory runs out Valkey REFUSES
|
||||
WRITES instead of discarding keys, so a full queue is an error in the
|
||||
application rather than jobs that quietly disappeared.
|
||||
{{- else }}
|
||||
Persistence with an evicting policy: the data survives a restart but not
|
||||
memory pressure. Fine for a cache that is expensive to rebuild — wrong for a
|
||||
Sidekiq or Celery queue, which would lose jobs while staying green.
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
{{- if eq .Values.valkey.maxmemoryPolicy "noeviction" }}
|
||||
READ THIS: no persistence AND noeviction is almost never intended. The data is
|
||||
gone on every pod restart, so nothing is being protected — but once memory is
|
||||
full, writes start FAILING instead of cold keys being dropped. For a cache set
|
||||
valkey.maxmemoryPolicy: allkeys-lru; for a queue set persistence.enabled: true.
|
||||
{{- else }}
|
||||
Cache shape: the dataset lives in the pod only. Both AOF and the RDB save
|
||||
points are switched off, so a restart starts from empty and nothing is written
|
||||
to ephemeral storage.
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.persistence.enabled }}
|
||||
|
||||
STORAGE
|
||||
{{ .Release.Name }}-{{ .Chart.Name }}-pvc, {{ .Values.persistence.size }}, {{ if .Values.persistence.storageClass }}storage class "{{ .Values.persistence.storageClass }}"{{ else }}on the cluster's default storage class
|
||||
(persistence.storageClass is unset — the default class is not encrypted){{ end }}.
|
||||
Persistence mode: {{ if .Values.persistence.appendonly }}append-only file (every write appended; a crash loses at most
|
||||
the last fsync interval){{ else }}RDB snapshots only (a crash loses every write since the last
|
||||
snapshot, silently — the instance comes back healthy, only shorter){{ end }}.
|
||||
|
||||
helm uninstall does NOT delete this PVC, which is deliberate — the data
|
||||
survives a reinstall of the same release name. It also means a release removed
|
||||
and recreated under a DIFFERENT name starts on an empty volume while the old
|
||||
one keeps occupying space. On encrypted Longhorn volumes that space is never
|
||||
returned to the pool.
|
||||
{{- end }}
|
||||
{{ if eq $replicas "0" }}
|
||||
SCALED TO ZERO
|
||||
{{ if .Values.scaleDown }}scaleDown: true{{ else }}valkey.replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so nothing answers on
|
||||
the address above and every application configured against it will fail its own
|
||||
checks. Nothing else is removed: Service and Secret stay{{ if .Values.persistence.enabled }}, the PVC keeps
|
||||
the data{{ end }}{{ if .Values.networkPolicy.enabled }}, and the NetworkPolicy stays in force{{ end }}.
|
||||
{{ end }}
|
||||
CHANGING THE PASSWORD IS NOT COVERED BY A CHECKSUM
|
||||
The pod template carries checksum/config over the configuration ConfigMap and
|
||||
checksum/secret-ref over the rendered OnePasswordItem. So a changed
|
||||
maxmemory/policy rolls the pod, and so does repointing secrets.authSecretRef at
|
||||
a DIFFERENT vault item. What cannot be covered is a new password inside the
|
||||
same item: Helm never sees the content of a Secret the 1Password operator
|
||||
fills in. After changing it there:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} rollout restart statefulset {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
|
||||
(operator.1password.io/auto-restart is not set on purpose — the operator
|
||||
restarts Deployments only, so on a StatefulSet it would be a silent no-op.)
|
||||
|
||||
Unverified, and named so rather than promised: because the probes read the
|
||||
password out of the same mounted file, the kubelet's refresh of that file
|
||||
(roughly a minute) should make the liveness probe fail against the still-old
|
||||
server and restart the container by itself. Do the rollout restart instead of
|
||||
waiting for it.
|
||||
{{ if .Values.networkPolicy.enabled }}
|
||||
NETWORK POLICY IS ACTIVE
|
||||
Ingress to port 6379 is allowed from namespace {{ .Release.Namespace }}{{ if .Values.networkPolicy.allowedNamespaces }} and from
|
||||
{{ join ", " .Values.networkPolicy.allowedNamespaces }}{{ end }}. Egress is unrestricted (this instance opens no
|
||||
outbound connection of its own).
|
||||
|
||||
It is only a boundary where the CNI enforces NetworkPolicies. Where it does
|
||||
not, the object is accepted and nothing is filtered — so verify it instead of
|
||||
reading it off the manifest:
|
||||
|
||||
kubectl -n <some-other-namespace> run t --rm -it --image=docker.io/valkey/valkey:{{ .Chart.AppVersion }} \
|
||||
-- valkey-cli -h {{ .Release.Name }}-{{ .Chart.Name }}-service.{{ .Release.Namespace }} ping
|
||||
|
||||
That must hang or be refused. "NOAUTH Authentication required." means the
|
||||
policy is not being enforced and the password is your only boundary.
|
||||
{{- else }}
|
||||
NO NETWORK POLICY
|
||||
networkPolicy.enabled is false, so the password is the only thing between this
|
||||
instance and every pod in the cluster. The official image compiles
|
||||
protected-mode off, so there is no second line of defence behind it.
|
||||
{{- end }}
|
||||
|
||||
WHAT A GREEN POD PROVES
|
||||
All three probes run "valkey-cli -e ping" AUTHENTICATED with the password from
|
||||
the mounted file and require the answer PONG. So Ready means: the server
|
||||
accepted a connection, accepted that password, and answered.
|
||||
|
||||
Two things it does not mean. It does not say anything about the dataset — a
|
||||
restarted instance whose AOF was lost answers PONG on an empty database. And
|
||||
it does not say your application's keys are reachable: that depends on the
|
||||
connection URL in the application's own vault item, which this chart neither
|
||||
writes nor reads.
|
||||
|
||||
The probe asserts the answer (PONG), not just that the command returned, and
|
||||
it passes "-e" so that valkey-cli reports an error reply through its exit code
|
||||
at all — without it a bare "valkey-cli ping" exits 0 while printing
|
||||
"NOAUTH Authentication required.", which would be a probe that cannot fail.
|
||||
@@ -1,434 +0,0 @@
|
||||
{{/*
|
||||
Render the body of a container "resources:" block from a values dict of the form
|
||||
|
||||
resources:
|
||||
limitFactor: 3 # optional, default 3
|
||||
requests: { cpu, memory, ephemeral-storage / ephemeralStorage, ... }
|
||||
limits: { ... } # optional, every entry optional
|
||||
|
||||
Rules (variant B, see issue #19):
|
||||
- requests are rendered as given (camelCase "ephemeralStorage" is normalized
|
||||
to the Kubernetes key "ephemeral-storage").
|
||||
- an explicitly set limit always wins.
|
||||
- a missing memory / ephemeral-storage limit is computed as
|
||||
limitFactor x the corresponding request.
|
||||
- a missing cpu limit is NOT set at all (the kubelet then treats CPU as
|
||||
unlimited; CPU is compressible, so this cannot kill the workload).
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. template-chart holds the reference implementation, so a
|
||||
fix belongs there as well and not here alone.
|
||||
*/}}
|
||||
{{- define "valkey.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
{{- $factor := 3.0 -}}
|
||||
{{- if not (kindIs "invalid" $resources.limitFactor) -}}
|
||||
{{- $factor = float64 $resources.limitFactor -}}
|
||||
{{- end -}}
|
||||
{{- $requests := dict -}}
|
||||
{{- range $key, $value := ($resources.requests | default dict) -}}
|
||||
{{- if not (kindIs "invalid" $value) -}}
|
||||
{{- $_ := set $requests (include "valkey.resourceKey" $key) $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $limits := dict -}}
|
||||
{{- range $key, $value := ($resources.limits | default dict) -}}
|
||||
{{- if not (kindIs "invalid" $value) -}}
|
||||
{{- $_ := set $limits (include "valkey.resourceKey" $key) $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- range $key, $request := $requests -}}
|
||||
{{- if and (ne $key "cpu") (not (hasKey $limits $key)) -}}
|
||||
{{- $_ := set $limits $key (include "valkey.multiplyQuantity" (dict "quantity" $request "factor" $factor)) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $out := dict -}}
|
||||
{{- with $requests }}{{- $_ := set $out "requests" . }}{{- end -}}
|
||||
{{- with $limits }}{{- $_ := set $out "limits" . }}{{- end -}}
|
||||
{{- toYaml $out -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Normalize a resource name from values to its Kubernetes key.
|
||||
*/}}
|
||||
{{- define "valkey.resourceKey" -}}
|
||||
{{- if eq . "ephemeralStorage" -}}ephemeral-storage{{- else -}}{{ . }}{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Multiply a Kubernetes quantity by a factor, keeping its suffix.
|
||||
Input: dict "quantity" <string|number> "factor" <float64>.
|
||||
Handles plain numbers ("2", "0.5", 1073741824), decimal ("1.5Gi"), binary
|
||||
("512Mi", "1Gi", "10Ki") and milli/SI suffixes ("100m", "500M"). Integer
|
||||
results are printed without a decimal point.
|
||||
*/}}
|
||||
{{- define "valkey.multiplyQuantity" -}}
|
||||
{{- $quantity := include "valkey.formatNumber" .quantity -}}
|
||||
{{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}}
|
||||
{{- if not $number -}}
|
||||
{{- fail (printf "cannot parse resource quantity %q" $quantity) -}}
|
||||
{{- end -}}
|
||||
{{- $suffix := trimPrefix $number $quantity -}}
|
||||
{{- if not (regexMatch "^(m|k|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$" $suffix) -}}
|
||||
{{- fail (printf "unsupported resource quantity suffix %q in %q" $suffix $quantity) -}}
|
||||
{{- end -}}
|
||||
{{- $product := mulf (float64 $number) .factor -}}
|
||||
{{- if eq $product (floor $product) -}}
|
||||
{{- printf "%d%s" (int64 $product) $suffix -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s%s" (include "valkey.formatNumber" $product) $suffix -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render a number as a plain decimal string, never in exponential notation.
|
||||
|
||||
Why this exists (issue #157): the null-safe fallback helpers pass values
|
||||
through fromYaml, which parses every number as float64. Go's "%v" switches
|
||||
float64 to exponential notation from about 1e6 upwards, so a bare byte count
|
||||
like 1048576 became "1.048576e+06" - not a valid Kubernetes quantity, and the
|
||||
render aborted. Strings ("512Mi") are passed through untouched.
|
||||
*/}}
|
||||
{{- define "valkey.formatNumber" -}}
|
||||
{{- if kindIs "float64" . -}}
|
||||
{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%v" . -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render the replica count for a Deployment/StatefulSet.
|
||||
|
||||
Input: dict
|
||||
"scaleDown" the chart-global .Values.scaleDown flag (may be nil)
|
||||
"replicas" the workload's replicas value (may be nil)
|
||||
|
||||
Rules (issue #25):
|
||||
- scaleDown: true takes precedence and always yields 0.
|
||||
- otherwise an explicitly set replicas value is used as given, including an
|
||||
explicit 0 — Helm's "default" would swallow 0, hence the explicit
|
||||
kindIs "invalid" nil check.
|
||||
- a missing replicas value defaults to 1.
|
||||
*/}}
|
||||
{{- define "valkey.replicas" -}}
|
||||
{{- if .scaleDown -}}
|
||||
0
|
||||
{{- else if not (kindIs "invalid" .replicas) -}}
|
||||
{{- .replicas -}}
|
||||
{{- else -}}
|
||||
1
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Merge a user-supplied values sub-tree onto a dict of chart defaults so that a
|
||||
null / empty override falls back to the defaults instead of erasing them.
|
||||
|
||||
Input: dict "defaults" <dict> "given" <dict|nil> "path" <string, optional
|
||||
values path used in error messages>. Output: YAML of the merged dict (consume
|
||||
it with fromYaml).
|
||||
|
||||
Why this exists (issues #61 and #99): Helm merges values maps recursively, but
|
||||
an override key written without children — e.g. a leftover
|
||||
|
||||
securityContext:
|
||||
|
||||
after deleting its last remaining sub-key — is YAML null. Where the chart
|
||||
defines a default, Helm treats that null as a deletion of the default; the
|
||||
template then renders "securityContext: null": no readOnlyRootFilesystem, no
|
||||
dropped capabilities, no allowPrivilegeEscalation: false. The workload starts
|
||||
and reports healthy while being unhardened, so nothing points at the mistake.
|
||||
The same shape erases a probe (it disappears) or the resources block (no
|
||||
requests, no computed limits). This helper defines the opposite semantics: an
|
||||
empty block means "chart defaults apply".
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
*/}}
|
||||
{{- define "valkey.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
{{- $given := .given -}}
|
||||
{{- $path := .path | default "value" -}}
|
||||
{{- if kindIs "invalid" $given -}}
|
||||
{{- $given = dict -}}
|
||||
{{- end -}}
|
||||
{{- if not (kindIs "map" $given) -}}
|
||||
{{- fail (printf "%s must be a mapping or null, got %s (%v)" $path (kindOf $given) $given) -}}
|
||||
{{- end -}}
|
||||
{{- $out := deepCopy $defaults -}}
|
||||
{{- range $key, $value := $given -}}
|
||||
{{- if not (kindIs "invalid" $value) -}}
|
||||
{{- $default := index $defaults $key -}}
|
||||
{{- if and (kindIs "map" $value) (kindIs "map" $default) -}}
|
||||
{{- $_ := set $out $key (fromYaml (include "valkey.defaultedDict" (dict "defaults" $default "given" $value "path" (printf "%s.%s" $path $key)))) -}}
|
||||
{{- else if kindIs "map" $default -}}
|
||||
{{- fail (printf "%s.%s must be a mapping or null, got %s (%v)" $path $key (kindOf $value) $value) -}}
|
||||
{{- else -}}
|
||||
{{- $_ := set $out $key $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- toYaml $out -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Pick one sub-block out of a values block that may itself be null.
|
||||
|
||||
Input: dict "block" <dict|nil> "key" <string> "path" <string>.
|
||||
Returns YAML of dict "value" <the sub-block, may be nil>, after rejecting a
|
||||
non-mapping block with a message that names the values path. Needed because
|
||||
".Values.valkey.securityContext" can itself be erased, and indexing into nil
|
||||
inside a template is not an error but silently yields nothing.
|
||||
*/}}
|
||||
{{- define "valkey.subBlock" -}}
|
||||
{{- $block := .block -}}
|
||||
{{- if kindIs "invalid" $block -}}
|
||||
{{- $block = dict -}}
|
||||
{{- end -}}
|
||||
{{- if not (kindIs "map" $block) -}}
|
||||
{{- fail (printf "%s must be a mapping or null, got %s (%v)" .path (kindOf $block) $block) -}}
|
||||
{{- end -}}
|
||||
{{- toYaml (dict "value" (index $block .key)) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The effective security contexts, probes and resources (issue #99).
|
||||
|
||||
The defaults below MUST stay in sync with the corresponding blocks in
|
||||
values.yaml, which stays the documented, user-facing place for them; this copy
|
||||
is only the fallback for the case where an override has erased the block. Every
|
||||
reason behind a value is written down there.
|
||||
|
||||
Two rules these fallbacks exist to honour (README, "Chart style guidelines"):
|
||||
|
||||
- Reproduce THIS chart's defaults, not the repo standard. The uid here is 999
|
||||
and the gid 1000, because that is the "valkey" user of the official image
|
||||
(9.1/alpine/Dockerfile: "addgroup -S -g 1000 valkey; adduser -S -G valkey
|
||||
-u 999 valkey"). Restoring the repo's usual 20000/1000 would leave the
|
||||
process unable to write its own data directory, and fsGroup has to keep
|
||||
matching runAsGroup or the PVC is not writable at all.
|
||||
- Carry the probe's CONTEXT, not just its timings. All three probes here are
|
||||
exec probes, so the command IS the probe - a fallback that lost it would
|
||||
leave the workload with no working check at all.
|
||||
*/}}
|
||||
{{- define "valkey.podSecurityContext" -}}
|
||||
{{- $given := (fromYaml (include "valkey.subBlock" (dict "block" . "key" "pod" "path" "valkey.securityContext"))).value -}}
|
||||
{{- $defaults := dict
|
||||
"runAsNonRoot" true
|
||||
"runAsUser" 999
|
||||
"runAsGroup" 1000
|
||||
"fsGroup" 1000
|
||||
"fsGroupChangePolicy" "Always"
|
||||
"seccompProfile" (dict "type" "RuntimeDefault") -}}
|
||||
{{- include "valkey.defaultedDict" (dict "defaults" $defaults "given" $given "path" "valkey.securityContext.pod") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "valkey.containerSecurityContext" -}}
|
||||
{{- $given := (fromYaml (include "valkey.subBlock" (dict "block" . "key" "container" "path" "valkey.securityContext"))).value -}}
|
||||
{{- $defaults := dict
|
||||
"allowPrivilegeEscalation" false
|
||||
"readOnlyRootFilesystem" true
|
||||
"runAsNonRoot" true
|
||||
"capabilities" (dict "drop" (list "ALL") "add" (list)) -}}
|
||||
{{- include "valkey.defaultedDict" (dict "defaults" $defaults "given" $given "path" "valkey.securityContext.container") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The three probes. All of them run the same script from the chart's ConfigMap,
|
||||
which authenticates with the password from the mounted auth file and asserts the
|
||||
answer PONG - see templates/valkey.configmap.yaml for why "-e" and
|
||||
VALKEYCLI_AUTH are what turn this from a formality into a check.
|
||||
*/}}
|
||||
{{- define "valkey.probeCommand" -}}
|
||||
{{- list "/bin/sh" "/etc/valkey/ping.sh" | toJson -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "valkey.livenessProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"exec" (dict "command" (include "valkey.probeCommand" . | fromJsonArray))
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 3 -}}
|
||||
{{- include "valkey.defaultedDict" (dict "defaults" $defaults "given" . "path" "valkey.livenessProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "valkey.readinessProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"exec" (dict "command" (include "valkey.probeCommand" . | fromJsonArray))
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 3 -}}
|
||||
{{- include "valkey.defaultedDict" (dict "defaults" $defaults "given" . "path" "valkey.readinessProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "valkey.startupProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"exec" (dict "command" (include "valkey.probeCommand" . | fromJsonArray))
|
||||
"periodSeconds" 5
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 30 -}}
|
||||
{{- include "valkey.defaultedDict" (dict "defaults" $defaults "given" . "path" "valkey.startupProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The merged resources block: the chart's defaults with the user's overrides on
|
||||
top, null-safe. Everything that needs the effective numbers goes through this,
|
||||
so the memory limit the configuration derives maxmemory from is the SAME number
|
||||
the container is given - not a second calculation that can drift from it.
|
||||
*/}}
|
||||
{{- define "valkey.mergedResources" -}}
|
||||
{{- $defaults := dict
|
||||
"limitFactor" 3
|
||||
"requests" (dict "cpu" 0.1 "memory" "256Mi" "ephemeral-storage" "64Mi") -}}
|
||||
{{- include "valkey.defaultedDict" (dict "defaults" $defaults "given" . "path" "valkey.resources") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "valkey.effectiveResources" -}}
|
||||
{{- include "valkey.resources" (fromYaml (include "valkey.mergedResources" .)) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Convert a Kubernetes memory quantity to a plain byte count.
|
||||
|
||||
Input: a quantity (string or number). Output: the byte count as an integer
|
||||
string. Needed because Valkey's maxmemory is compared against, and derived
|
||||
from, the container's memory limit - and "576Mi" cannot be compared with
|
||||
"1Gi" as text.
|
||||
|
||||
The "m" (milli) suffix is rejected rather than converted: a milli-byte memory
|
||||
quantity is legal Kubernetes and meaningless here, and silently rounding it to
|
||||
zero would render "maxmemory 0", which in Valkey means UNLIMITED - the exact
|
||||
opposite of what this chart computes a number for.
|
||||
*/}}
|
||||
{{- define "valkey.quantityBytes" -}}
|
||||
{{- $quantity := include "valkey.formatNumber" . -}}
|
||||
{{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}}
|
||||
{{- if not $number -}}
|
||||
{{- fail (printf "cannot parse memory quantity %q" $quantity) -}}
|
||||
{{- end -}}
|
||||
{{- $suffix := trimPrefix $number $quantity -}}
|
||||
{{- $factors := dict
|
||||
"" 1.0
|
||||
"k" 1000.0 "M" 1000000.0 "G" 1000000000.0 "T" 1000000000000.0
|
||||
"Ki" 1024.0 "Mi" 1048576.0 "Gi" 1073741824.0 "Ti" 1099511627776.0 -}}
|
||||
{{- if not (hasKey $factors $suffix) -}}
|
||||
{{- fail (printf "unsupported memory quantity suffix %q in %q (use one of k, M, G, T, Ki, Mi, Gi, Ti, or plain bytes)" $suffix $quantity) -}}
|
||||
{{- end -}}
|
||||
{{- printf "%d" (int64 (mulf (float64 $number) (index $factors $suffix))) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The container's effective memory limit, as a byte count.
|
||||
|
||||
It is read back out of the RENDERED resources block on purpose: an explicitly
|
||||
set limits.memory wins there, and a missing one is limitFactor x the request -
|
||||
so this cannot disagree with what the container actually gets.
|
||||
*/}}
|
||||
{{- define "valkey.memoryLimitBytes" -}}
|
||||
{{- $block := fromYaml (include "valkey.effectiveResources" .) -}}
|
||||
{{- $limit := "" -}}
|
||||
{{- if $block.limits -}}
|
||||
{{- $limit = index $block.limits "memory" -}}
|
||||
{{- end -}}
|
||||
{{- if kindIs "invalid" $limit -}}
|
||||
{{- fail "valkey: no memory limit could be determined. valkey.resources needs either requests.memory (the limit is then limitFactor x the request) or an explicit limits.memory - maxmemory is derived from it, and an unbounded instance would be OOM-killed instead of evicting." -}}
|
||||
{{- end -}}
|
||||
{{- include "valkey.quantityBytes" $limit -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Valkey's maxmemory for this release, as a byte count.
|
||||
|
||||
Input: the .Values.valkey block.
|
||||
|
||||
- valkey.maxmemory set -> used as given, AFTER checking it against the
|
||||
container's memory limit. A value above the limit ABORTS the render: it
|
||||
would hand the decision "what happens when memory runs out" from the
|
||||
eviction policy to the kubelet's OOM killer, and that arrives as a
|
||||
restarted pod with no explanation. Not clamped, because a clamp would mean
|
||||
the running instance quietly disagrees with the values file.
|
||||
- otherwise -> maxmemoryFactor x the limit, floored. The
|
||||
remaining fraction is not slack: client output buffers, the AOF buffer and
|
||||
above all the copy-on-write pages of the fork() that snapshots or rewrites
|
||||
the AOF live outside maxmemory.
|
||||
*/}}
|
||||
{{- define "valkey.maxmemoryBytes" -}}
|
||||
{{- $limitBytes := int64 (include "valkey.memoryLimitBytes" .resources) -}}
|
||||
{{- if and (not (kindIs "invalid" .maxmemory)) (ne (printf "%v" .maxmemory) "") -}}
|
||||
{{- $wanted := int64 (include "valkey.quantityBytes" .maxmemory) -}}
|
||||
{{- if le $wanted (int64 0) -}}
|
||||
{{- fail (printf "valkey.maxmemory is %v, which converts to %d bytes. In Valkey, maxmemory 0 means UNLIMITED, so this would switch off the very bound the chart derives - leave valkey.maxmemory unset to use maxmemoryFactor x the memory limit instead." .maxmemory $wanted) -}}
|
||||
{{- end -}}
|
||||
{{- if gt $wanted $limitBytes -}}
|
||||
{{- fail (printf "valkey.maxmemory is %v (%d bytes) and thus ABOVE the container's memory limit (%d bytes). Valkey would keep accepting data until the kubelet OOM-kills the pod, which is what the eviction policy exists to prevent. Lower valkey.maxmemory, or raise valkey.resources.requests.memory / limitFactor / limits.memory." .maxmemory $wanted $limitBytes) -}}
|
||||
{{- end -}}
|
||||
{{- printf "%d" $wanted -}}
|
||||
{{- else -}}
|
||||
{{- $factor := 0.75 -}}
|
||||
{{- if not (kindIs "invalid" .maxmemoryFactor) -}}
|
||||
{{- $factor = float64 .maxmemoryFactor -}}
|
||||
{{- end -}}
|
||||
{{- $derived := int64 (floor (mulf (float64 $limitBytes) $factor)) -}}
|
||||
{{- if le $derived (int64 0) -}}
|
||||
{{- fail (printf "valkey.maxmemoryFactor %v applied to the memory limit (%d bytes) yields %d bytes. Valkey reads maxmemory 0 as UNLIMITED, so this would silently remove the bound." .maxmemoryFactor $limitBytes $derived) -}}
|
||||
{{- end -}}
|
||||
{{- printf "%d" $derived -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render a byte count as a human-readable size, for NOTES.txt and comments only.
|
||||
Never used where an exact value matters.
|
||||
*/}}
|
||||
{{- define "valkey.humanBytes" -}}
|
||||
{{- $bytes := float64 . -}}
|
||||
{{- if ge $bytes 1073741824.0 -}}
|
||||
{{- printf "%.2fGi" (divf $bytes 1073741824.0) -}}
|
||||
{{- else if ge $bytes 1048576.0 -}}
|
||||
{{- printf "%.2fMi" (divf $bytes 1048576.0) -}}
|
||||
{{- else if ge $bytes 1024.0 -}}
|
||||
{{- printf "%.2fKi" (divf $bytes 1024.0) -}}
|
||||
{{- else -}}
|
||||
{{- printf "%d bytes" (int64 $bytes) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Additive environment entries for the Valkey container.
|
||||
|
||||
Input: dict "root" <root context> "env" <the env list>.
|
||||
Entries are rendered through tpl, so a value may contain Helm templating, and
|
||||
the value, secretKeyRef and configMapKeyRef forms are supported.
|
||||
*/}}
|
||||
{{- define "valkey.env" -}}
|
||||
{{- $root := .root -}}
|
||||
{{- range $value := .env }}
|
||||
{{- if $value.value }}
|
||||
- name: {{ $value.name | quote }}
|
||||
# quote is required, not cosmetic: a value like "6379" or "true" renders
|
||||
# unquoted as a YAML number/bool, and the API server then rejects the whole
|
||||
# manifest with "cannot unmarshal number into Go struct field EnvVar.value of
|
||||
# type string".
|
||||
value: {{ tpl $value.value $root | quote }}
|
||||
{{- else if $value.valueFrom }}
|
||||
{{- if $value.valueFrom.secretKeyRef }}
|
||||
- name: {{ $value.name }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ tpl $value.valueFrom.secretKeyRef.name $root | quote }}
|
||||
key: {{ tpl $value.valueFrom.secretKeyRef.key $root | quote }}
|
||||
{{- else if $value.valueFrom.configMapKeyRef }}
|
||||
- name: {{ $value.name }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ tpl $value.valueFrom.configMapKeyRef.name $root | quote }}
|
||||
key: {{ tpl $value.valueFrom.configMapKeyRef.key $root | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
@@ -1,164 +0,0 @@
|
||||
{{- $maxmemory := include "valkey.maxmemoryBytes" (dict "resources" .Values.valkey.resources "maxmemory" .Values.valkey.maxmemory "maxmemoryFactor" .Values.valkey.maxmemoryFactor) -}}
|
||||
{{- $limitBytes := include "valkey.memoryLimitBytes" .Values.valkey.resources -}}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: "{{ .Release.Name }}-{{ .Chart.Name }}-configmap"
|
||||
data:
|
||||
# The server's whole configuration. It is mounted as a directory at
|
||||
# /etc/valkey and passed to the image as an argument, so the entrypoint starts
|
||||
# "valkey-server /etc/valkey/valkey.conf".
|
||||
#
|
||||
# The pod template carries checksum/config over this ConfigMap: without it a
|
||||
# changed configuration would restart nothing, and Valkey reads its
|
||||
# configuration exactly once, at start.
|
||||
valkey.conf: |
|
||||
# Rendered by the valkey Helm chart - do not edit in the cluster, the next
|
||||
# helm upgrade overwrites it. Every value below comes from values.yaml.
|
||||
|
||||
port 6379
|
||||
|
||||
# The data directory. It is also the image's WORKDIR, so this is only made
|
||||
# explicit for whoever reads the file; it is a PVC when
|
||||
# persistence.enabled is true and an emptyDir otherwise.
|
||||
dir /data
|
||||
|
||||
# maxmemory is derived from the container's memory limit
|
||||
# ({{ $limitBytes }} bytes = {{ include "valkey.humanBytes" $limitBytes }}), not configured independently of it:
|
||||
# {{ if kindIs "invalid" .Values.valkey.maxmemory }}maxmemoryFactor {{ .Values.valkey.maxmemoryFactor | default 0.75 }} x that limit{{ else }}valkey.maxmemory was set explicitly and checked against that limit{{ end }}.
|
||||
# The remainder of the limit is for what maxmemory does NOT cover: client
|
||||
# output buffers, the AOF buffer, and the copy-on-write pages of the fork()
|
||||
# that writes a snapshot or rewrites the AOF.
|
||||
maxmemory {{ $maxmemory }}
|
||||
maxmemory-policy {{ required "valkey.maxmemoryPolicy must be set (see values.yaml for the two sensible pairs)" .Values.valkey.maxmemoryPolicy }}
|
||||
{{- if .Values.persistence.enabled }}
|
||||
|
||||
{{- if .Values.persistence.appendonly }}
|
||||
# Append-only file: every write is appended, so a crash loses at most the
|
||||
# last fsync interval instead of everything since the last snapshot. The
|
||||
# RDB save points stay at their defaults, as a second, cheaper copy.
|
||||
appendonly yes
|
||||
{{- else }}
|
||||
# persistence.appendonly is false: the dataset survives only as periodic RDB
|
||||
# snapshots (Valkey's default save points), so a crash loses every write
|
||||
# since the last one - and it does so silently, because the instance comes
|
||||
# back looking healthy, only shorter.
|
||||
appendonly no
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
|
||||
# persistence.enabled is false: /data is an emptyDir, so nothing written
|
||||
# there survives a pod restart anyway. Both persistence mechanisms are
|
||||
# switched off rather than left writing into ephemeral storage - "save """
|
||||
# disables the RDB save points, which are on by default.
|
||||
appendonly no
|
||||
save ""
|
||||
{{- end }}
|
||||
{{- with .Values.valkey.extraConfig }}
|
||||
|
||||
# valkey.extraConfig, verbatim.
|
||||
{{ tpl . $ | trim | nindent 4 }}
|
||||
{{- end }}
|
||||
|
||||
# The password, as a complete configuration line out of the 1Password item
|
||||
# (field "auth-conf"), mounted as a file. Two things about this line are
|
||||
# deliberate:
|
||||
#
|
||||
# - it is an "include" rather than a "requirepass" written here, so the
|
||||
# password is never part of this ConfigMap, of the process list, or of
|
||||
# anything Helm renders;
|
||||
# - it is LAST. In a Valkey configuration the last occurrence of a
|
||||
# directive wins, so nothing in valkey.extraConfig above can switch the
|
||||
# password off, by accident or otherwise.
|
||||
#
|
||||
# An init container has already verified that this file carries a
|
||||
# "requirepass <password>" line before the server ever starts.
|
||||
include /etc/valkey-auth/auth.conf
|
||||
|
||||
# The probe command for all three probes. A script rather than an inline shell
|
||||
# one-liner, because the two things that make it a real check need to be
|
||||
# written down next to them.
|
||||
ping.sh: |
|
||||
#!/bin/sh
|
||||
# Authenticated liveness/readiness/startup check for Valkey.
|
||||
#
|
||||
# WHY "-e" AND grep, i.e. two assertions rather than one: a bare
|
||||
# "valkey-cli ping" probe CANNOT FAIL. Read off the source (valkey 9.1.2,
|
||||
# src/valkey-cli.c): cliReadReply turns an error reply into exit(1) in
|
||||
# exactly one branch, guarded by config.set_errcode, and "-e" is the only
|
||||
# thing that sets it ("-e Return exit error code when command execution
|
||||
# fails."). Without -e the command prints "NOAUTH Authentication required."
|
||||
# and exits 0 - green, forever, whatever the server does.
|
||||
#
|
||||
# Either half alone would catch that: "grep -qx PONG" asserts the ANSWER, so
|
||||
# it fails on an error reply even without -e. Both are here on purpose. The
|
||||
# grep is the assertion; "-e" makes the failure explicit and, because the
|
||||
# error then goes to stderr instead of stdout, puts the server's own message
|
||||
# ("NOAUTH...", "WRONGPASS...") into the probe's failure output where
|
||||
# kubectl describe shows it.
|
||||
#
|
||||
# WHY THE PASSWORD COMES THROUGH THE ENVIRONMENT: "valkey-cli -a <password>"
|
||||
# puts the password in the process list, where every container in this pod
|
||||
# can read it. VALKEYCLI_AUTH is valkey-cli's own environment variable for
|
||||
# it (REDISCLI_AUTH is still honoured as the legacy spelling), and it is set
|
||||
# only for the duration of this one command.
|
||||
#
|
||||
# Nothing here prints the password: it only ever lives in a shell variable,
|
||||
# and valkey-cli's own error messages do not echo it.
|
||||
set -eu
|
||||
|
||||
AUTH_FILE=/etc/valkey-auth/auth.conf
|
||||
|
||||
# The same file the server was configured from, so probe and server can
|
||||
# never disagree about the password. sed's character class is written the
|
||||
# POSIX way (no \+): this runs in the image's BusyBox shell.
|
||||
pass="$(sed -n 's/^requirepass[[:space:]][[:space:]]*//p' "$AUTH_FILE" | head -n 1)"
|
||||
|
||||
# Valkey's configuration parser allows the value to be quoted; strip one
|
||||
# surrounding pair of double quotes if present.
|
||||
case "$pass" in
|
||||
'"'*'"') pass="${pass#\"}"; pass="${pass%\"}" ;;
|
||||
esac
|
||||
|
||||
VALKEYCLI_AUTH="$pass" valkey-cli -e -p 6379 ping | grep -qx PONG
|
||||
|
||||
# The init container's check. It validates the mounted auth file and does NOT
|
||||
# assemble it - the password is a finished configuration line from the vault.
|
||||
check-auth.sh: |
|
||||
#!/bin/sh
|
||||
# Refuse to start unless the mounted auth file really carries a
|
||||
# "requirepass <password>" line.
|
||||
#
|
||||
# The failure this prevents: the password arrives as a ready-made
|
||||
# configuration line from a 1Password field, and a misspelled key
|
||||
# ("requirepas"), a stray quote or an empty field would leave Valkey running
|
||||
# WITHOUT any password - no render error, no crash, a green pod, and an open
|
||||
# server (the official image compiles protected-mode off). Of all the ways
|
||||
# this chart can be misconfigured, that is the only one nothing else would
|
||||
# report.
|
||||
#
|
||||
# It never prints the file: an error message containing the password would
|
||||
# put it in the pod's event log and in every CI diagnostics dump.
|
||||
set -eu
|
||||
|
||||
AUTH_FILE=/etc/valkey-auth/auth.conf
|
||||
|
||||
if [ ! -r "$AUTH_FILE" ]; then
|
||||
echo "valkey: $AUTH_FILE is missing or unreadable." >&2
|
||||
echo " The 1Password item behind secrets.authSecretRef must carry a field" >&2
|
||||
echo " named 'auth-conf'. The 1Password operator turns each field of the" >&2
|
||||
echo " item into one key of the Kubernetes Secret, so a differently named" >&2
|
||||
echo " field produces no such file." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q '^requirepass[[:space:]][[:space:]]*[^[:space:]]' "$AUTH_FILE"; then
|
||||
echo "valkey: the mounted auth file carries no 'requirepass <password>' line." >&2
|
||||
echo " Expected the field 'auth-conf' of the 1Password item to contain" >&2
|
||||
echo " exactly one configuration line:" >&2
|
||||
echo " requirepass <the password>" >&2
|
||||
echo " Refusing to start: Valkey would come up without a password, report" >&2
|
||||
echo " healthy, and accept connections from the whole pod network." >&2
|
||||
echo " (The file's contents are deliberately not printed.)" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,34 +0,0 @@
|
||||
{{- if .Values.networkPolicy.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
# Ingress only. Egress is deliberately unrestricted: this instance opens no
|
||||
# outbound connection of its own (no replication, no cluster bus), so an
|
||||
# egress rule here would constrain nothing while making the object look
|
||||
# stricter than it is.
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
# One rule with a list of sources. Note the shape: an empty "from" would
|
||||
# allow EVERYTHING, so the namespaces are always spelled out - the release's
|
||||
# own namespace first, then whatever networkPolicy.allowedNamespaces adds.
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
# Set by Kubernetes itself on every namespace (>= 1.21), so this
|
||||
# needs no labelling work on the target namespaces.
|
||||
kubernetes.io/metadata.name: {{ .Release.Namespace }}
|
||||
{{- range .Values.networkPolicy.allowedNamespaces }}
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: {{ . | quote }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6379
|
||||
{{- end }}
|
||||
@@ -1,19 +0,0 @@
|
||||
{{- if .Values.persistence.enabled }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
# Deliberately NOT null-safe: an erased persistence block yields an
|
||||
# invalid PVC that is rejected at apply time, which is loud. A chart-side
|
||||
# fallback would silently size somebody's queue volume - and on an
|
||||
# existing release it could not help anyway, because a PVC cannot shrink.
|
||||
storage: {{ .Values.persistence.size }}
|
||||
{{- if .Values.persistence.storageClass }}
|
||||
storageClassName: {{ .Values.persistence.storageClass }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,11 +0,0 @@
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
||||
spec:
|
||||
# The item must carry the field "auth-conf", holding the complete
|
||||
# configuration line "requirepass <password>" - the 1Password operator
|
||||
# materializes one Secret key per item field, so the field NAME is what the
|
||||
# chart depends on. NOTES.txt prints this, and the init container refuses to
|
||||
# start the pod if the line is not there.
|
||||
itemPath: {{ required "You must provide an auth Secret Reference (secrets.authSecretRef) - the 1Password item holding this instance's 'requirepass <password>' line" .Values.secrets.authSecretRef }}
|
||||
@@ -1,18 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
spec:
|
||||
selector:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6379
|
||||
targetPort: valkey
|
||||
name: valkey
|
||||
# ClusterIP, and there is deliberately no value to change it: this chart
|
||||
# deploys one application's cache or queue backend. A NodePort or LoadBalancer
|
||||
# would put a Valkey whose only authentication is one password on a routable
|
||||
# address. Applications reach it at
|
||||
# <release>-valkey-service.<namespace>.svc.cluster.local:6379
|
||||
type: ClusterIP
|
||||
@@ -1,162 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
spec:
|
||||
replicas: {{ include "valkey.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.valkey.replicas) }}
|
||||
serviceName: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
template:
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
annotations:
|
||||
# Hash of the rendered ConfigMap: a changed configuration changes the pod
|
||||
# template and thus restarts the pod. Without it a changed maxmemory or
|
||||
# eviction policy would apply to nothing - Valkey reads its
|
||||
# configuration once, at start, and a mounted file is not re-read.
|
||||
checksum/config: {{ include (print $.Template.BasePath "/valkey.configmap.yaml") . | sha256sum }}
|
||||
# Hash of the rendered OnePasswordItem. This covers the SOURCE of the
|
||||
# password, not its content: repointing secrets.authSecretRef at a
|
||||
# different vault item rolls the pod instead of leaving the old password
|
||||
# in the running server. A change INSIDE the item cannot be covered -
|
||||
# Helm never sees a secret the 1Password operator materializes (see
|
||||
# values.yaml).
|
||||
#
|
||||
# operator.1password.io/auto-restart is deliberately NOT set: the
|
||||
# operator only restarts Deployments, so on a StatefulSet it is a silent
|
||||
# no-op that advertises a guarantee which does not exist.
|
||||
checksum/secret-ref: {{ include (print $.Template.BasePath "/valkey.secret.yaml") . | sha256sum }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
{{- include "valkey.podSecurityContext" .Values.valkey.securityContext | nindent 8 }}
|
||||
initContainers:
|
||||
# Validates the mounted auth file and nothing else. It does not compose
|
||||
# the configuration line - that comes finished out of the vault - it only
|
||||
# refuses to let the pod start when the line is missing, which is the one
|
||||
# misconfiguration of this chart that would otherwise produce a healthy,
|
||||
# password-less, network-reachable Valkey. The script, and the reason it
|
||||
# never prints the file, are in templates/valkey.configmap.yaml.
|
||||
#
|
||||
# Same image as the server, so there is no second image to pin, no
|
||||
# Renovate block to maintain for it and nothing extra to pull.
|
||||
- name: {{ .Release.Name }}-{{ .Chart.Name }}-check-auth
|
||||
image: "docker.io/valkey/valkey:{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
# command, not args: args would be handed to the image's entrypoint,
|
||||
# which starts valkey-server for anything that looks like a config
|
||||
# file or an option.
|
||||
command:
|
||||
- /bin/sh
|
||||
- /etc/valkey/check-auth.sh
|
||||
securityContext:
|
||||
{{- include "valkey.containerSecurityContext" .Values.valkey.securityContext | nindent 12 }}
|
||||
# Fixed and deliberately small: this container runs one grep. Not
|
||||
# values-configurable - there is nothing here to tune.
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 16Mi
|
||||
ephemeral-storage: 1Mi
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 32Mi
|
||||
ephemeral-storage: 10Mi
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/valkey
|
||||
readOnly: true
|
||||
- name: auth
|
||||
mountPath: /etc/valkey-auth
|
||||
readOnly: true
|
||||
containers:
|
||||
- name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
image: "docker.io/valkey/valkey:{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
# args, NOT command: the image's entrypoint is
|
||||
# "tini -- docker-entrypoint.sh", and replacing it would lose tini as
|
||||
# PID 1 (zombie reaping) and the entrypoint's umask. An argument
|
||||
# ending in .conf makes the entrypoint start
|
||||
# "valkey-server /etc/valkey/valkey.conf", which is exactly what is
|
||||
# wanted. Note which branch of that entrypoint is NOT taken: running as
|
||||
# uid 999 skips its root-only "chown /data + setpriv" step, so the
|
||||
# volume is made writable by fsGroup instead.
|
||||
args:
|
||||
- /etc/valkey/valkey.conf
|
||||
# The chart sets no environment variable of its own - everything the
|
||||
# server is told comes from its configuration file, which is what keeps
|
||||
# the password out of the environment. So the block is rendered only
|
||||
# when the operator actually adds an entry, instead of leaving an empty
|
||||
# "env:" (i.e. env: null) in the manifest.
|
||||
{{- with .Values.valkey.env }}
|
||||
env:
|
||||
{{- include "valkey.env" (dict "root" $ "env" .) | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
protocol: TCP
|
||||
name: valkey
|
||||
# Probes, resources and both security contexts go through the
|
||||
# null-safe helpers (#99): an erased block means "chart defaults
|
||||
# apply" instead of a vanished probe or an unhardened container.
|
||||
# Never wrap these in an "if" - a guard hides the erased case
|
||||
# completely instead of rendering a visible null.
|
||||
livenessProbe:
|
||||
{{- include "valkey.livenessProbe" .Values.valkey.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- include "valkey.readinessProbe" .Values.valkey.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- include "valkey.startupProbe" .Values.valkey.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "valkey.effectiveResources" .Values.valkey.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- include "valkey.containerSecurityContext" .Values.valkey.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
# The image's WORKDIR. Everything Valkey writes goes here (the AOF
|
||||
# directory, RDB snapshots), and it is the only writable path the
|
||||
# server needs - which is what lets readOnlyRootFilesystem stay true
|
||||
# without a single extra emptyDir.
|
||||
- name: data
|
||||
mountPath: /data
|
||||
- name: config
|
||||
mountPath: /etc/valkey
|
||||
readOnly: true
|
||||
# The auth file, mounted as a DIRECTORY rather than with subPath, so
|
||||
# the kubelet keeps it up to date when the 1Password operator writes
|
||||
# a new password into the Secret. That does not restart the server by
|
||||
# itself (see the annotations above).
|
||||
- name: auth
|
||||
mountPath: /etc/valkey-auth
|
||||
readOnly: true
|
||||
restartPolicy: Always
|
||||
volumes:
|
||||
- name: data
|
||||
{{- if .Values.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Release.Name }}-{{ .Chart.Name }}-pvc
|
||||
{{- else }}
|
||||
# persistence.enabled is false: the dataset lives in the pod and is
|
||||
# gone with it. The configuration switches both AOF and the RDB save
|
||||
# points off in that case, so this emptyDir stays empty - it exists
|
||||
# because the image's WORKDIR has to be writable.
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
- name: config
|
||||
configMap:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-configmap
|
||||
- name: auth
|
||||
secret:
|
||||
secretName: {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
||||
items:
|
||||
# The 1Password item's field name on the left, the file name on the
|
||||
# right. Spelled out rather than mounting the whole Secret, so a
|
||||
# missing field fails the pod with a named reference instead of
|
||||
# producing a directory without the file.
|
||||
- key: auth-conf
|
||||
path: auth.conf
|
||||
@@ -1,223 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||
"title": "valkey values",
|
||||
"description": "Values schema for the valkey chart (issue #68, guideline 12). additionalProperties is false on every chart-owned object level, so a misspelled key fails the render instead of being silently ignored. The objects that go verbatim into the pod spec (probes, security contexts, global) stay open, because the chart does not interpret their contents. secrets.authSecretRef has no valid default and is therefore enforced by required() in the template, not here - a required array next to a key that ships as null could never fire.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"global": {
|
||||
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
|
||||
"type": "object"
|
||||
},
|
||||
"scaleDown": {
|
||||
"description": "true renders the StatefulSet with replicas: 0 and overrides valkey.replicas. PVC, Secret, Service and NetworkPolicy stay in place.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"secrets": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"authSecretRef": {
|
||||
"description": "REQUIRED (enforced by required() in the template). op:// path of the 1Password item carrying the field 'auth-conf' with the complete configuration line 'requirepass <password>'.",
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"valkey": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"replicas": {
|
||||
"description": "0 or 1. This chart runs exactly one Valkey: a second pod would fight over the same ReadWriteOnce volume, and clients behind the Service would silently see two different datasets.",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"resources": {
|
||||
"$ref": "#/definitions/resources"
|
||||
},
|
||||
"maxmemoryFactor": {
|
||||
"description": "Fraction of the container's computed memory limit that Valkey may fill with data. The rest covers what maxmemory does not: client buffers, the AOF buffer and the copy-on-write pages of the snapshot/AOF-rewrite fork. Ignored when valkey.maxmemory is set.",
|
||||
"type": ["number", "null"],
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"maxmemory": {
|
||||
"description": "Explicit maxmemory as a Kubernetes quantity (e.g. '512Mi'), overriding maxmemoryFactor. A value above the container's memory limit aborts the render. Valkey's own spellings ('512mb') are rejected on purpose - one quantity syntax per chart.",
|
||||
"type": ["string", "null"],
|
||||
"pattern": "^[0-9]+(\\.[0-9]+)?(k|M|G|T|Ki|Mi|Gi|Ti)?$"
|
||||
},
|
||||
"maxmemoryPolicy": {
|
||||
"description": "Valkey's maxmemory-policy. Enumerated rather than free text: a typo would make the server refuse to start, and the enum names the value instead. 'noeviction' for queues (the default), 'allkeys-lru' for caches.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"noeviction",
|
||||
"allkeys-lru",
|
||||
"allkeys-lfu",
|
||||
"allkeys-random",
|
||||
"volatile-lru",
|
||||
"volatile-lfu",
|
||||
"volatile-random",
|
||||
"volatile-ttl"
|
||||
]
|
||||
},
|
||||
"extraConfig": {
|
||||
"description": "Extra configuration lines, appended verbatim (rendered through tpl). Cannot switch the password off: the auth include is rendered after it and the last directive wins.",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"livenessProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"readinessProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"startupProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"securityContext": {
|
||||
"$ref": "#/definitions/securityContext"
|
||||
},
|
||||
"env": {
|
||||
"$ref": "#/definitions/env"
|
||||
}
|
||||
}
|
||||
},
|
||||
"persistence": {
|
||||
"description": "The data volume. Deliberately NOT null-safe: an erased block yields an invalid PVC, which fails at apply time instead of sizing somebody's queue volume by chart default. Issue #99 covers the silent class of failure, and this one is loud.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "true = PVC, false = emptyDir (and the chart then switches AOF and the RDB save points off). Pairs with valkey.maxmemoryPolicy - see values.yaml.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"appendonly": {
|
||||
"description": "AOF instead of RDB-only. Has no effect while persistence.enabled is false: there is nothing to append to, and the chart writes 'appendonly no' plus 'save \"\"'.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"size": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"storageClass": {
|
||||
"description": "null uses the cluster's default class - which on this cluster is NOT encrypted, so leaving the default 'longhorn-encrypted-ns' is a decision about data at rest.",
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"networkPolicy": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "On by default. Only a real boundary where the cluster's CNI enforces NetworkPolicies - an unenforced policy is accepted by the API server and filters nothing.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowedNamespaces": {
|
||||
"description": "Namespaces allowed in addition to the release's own, matched on the kubernetes.io/metadata.name label Kubernetes sets itself. A name that does not exist matches nothing and is silently ineffective.",
|
||||
"type": ["array", "null"],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"resources": {
|
||||
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting. The memory limit computed here is also what valkey.maxmemory is derived from and checked against.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"limitFactor": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"requests": {
|
||||
"$ref": "#/definitions/resourceList"
|
||||
},
|
||||
"limits": {
|
||||
"$ref": "#/definitions/resourceList"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourceList": {
|
||||
"description": "Both spellings of the ephemeral-storage key are accepted because the resources helper normalizes camelCase to the Kubernetes key.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"cpu": {
|
||||
"type": ["string", "number", "null"]
|
||||
},
|
||||
"memory": {
|
||||
"type": ["string", "number", "null"]
|
||||
},
|
||||
"ephemeral-storage": {
|
||||
"type": ["string", "number", "null"]
|
||||
},
|
||||
"ephemeralStorage": {
|
||||
"type": ["string", "number", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"type": ["array", "null"],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueFrom": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"secretKeyRef": {
|
||||
"$ref": "#/definitions/keyRef"
|
||||
},
|
||||
"configMapKeyRef": {
|
||||
"$ref": "#/definitions/keyRef"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"keyRef": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "key"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityContext": {
|
||||
"description": "May be null / empty at any level: the chart then falls back to its hardening defaults (issue #99). This chart's fallback reproduces ITS deviation, not the repo standard - uid 999 / gid 1000 are the image's own 'valkey' user, and fsGroup has to keep matching runAsGroup or the data volume is not writable.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"pod": {
|
||||
"description": "Merged onto the chart's pod hardening defaults; kept open because the chart does not interpret the individual fields. seccompProfile lives here so it also covers the init container.",
|
||||
"type": ["object", "null"]
|
||||
},
|
||||
"container": {
|
||||
"description": "Merged onto the chart's container hardening defaults and applied to BOTH the server and the auth-check init container; kept open because the chart does not interpret the individual fields.",
|
||||
"type": ["object", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"probe": {
|
||||
"description": "Merged onto the chart's probe defaults and rendered as a container probe; kept open because the chart does not interpret the individual fields. May be null / empty, which means the defaults apply (issue #99) rather than the probe disappearing. The default carries the exec command - for these probes the command IS the check (guideline 12.5).",
|
||||
"type": ["object", "null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
# One Valkey instance for ONE application - a cache or a queue backend, not a
|
||||
# shared database. There is deliberately no cluster mode, no replication and no
|
||||
# Ingress: a Valkey instance belongs on the cluster network, reachable through
|
||||
# its ClusterIP Service, and everything that would expose it to the internet is
|
||||
# absent rather than switched off.
|
||||
#
|
||||
# The two shapes this chart is meant for, and they are PAIRS of values, not two
|
||||
# independent switches:
|
||||
#
|
||||
# QUEUE (the chart default) - Sidekiq, Celery, anything whose entries are work
|
||||
# that must not vanish:
|
||||
# persistence.enabled: true
|
||||
# valkey.maxmemoryPolicy: noeviction
|
||||
#
|
||||
# CACHE - recomputable data only:
|
||||
# persistence.enabled: false
|
||||
# valkey.maxmemoryPolicy: allkeys-lru
|
||||
#
|
||||
# Mixing them is what this comment exists to prevent. A queue with
|
||||
# allkeys-lru throws away jobs under memory pressure while every application
|
||||
# stays green - one of the hypotheses in cluster-config#102, and cheaper to
|
||||
# prevent here than to diagnose there. And "persistence.enabled: false" together
|
||||
# with the default "noeviction" is almost never what anyone wants: a full cache
|
||||
# then starts REFUSING WRITES instead of dropping cold keys, and after a pod
|
||||
# restart it is empty anyway - so the strictness buys nothing and costs write
|
||||
# errors.
|
||||
|
||||
# Scale the whole chart down without uninstalling it: scaleDown: true renders the
|
||||
# StatefulSet with replicas: 0 and takes precedence over valkey.replicas. The
|
||||
# PVC, the Secret, the Service and the NetworkPolicy stay in place;
|
||||
# scaleDown: false brings the workload back up.
|
||||
scaleDown: false
|
||||
|
||||
secrets:
|
||||
# op:// path of the 1Password item holding this instance's password.
|
||||
#
|
||||
# The item must carry ONE field, named "auth-conf", whose value is a complete
|
||||
# Valkey configuration line:
|
||||
#
|
||||
# requirepass <the password>
|
||||
#
|
||||
# Not the bare password: the chart mounts this field as a file and the main
|
||||
# configuration "include"s it, so the password never appears in the process
|
||||
# list, in an environment variable, or anywhere Helm can see it. That is also
|
||||
# why no init container assembles the line - the vault is the only place the
|
||||
# secret exists in.
|
||||
#
|
||||
# A ready-made configuration line has one failure mode, and the chart closes
|
||||
# it: a misspelled key ("requirepas"), a stray quote or an empty field would
|
||||
# leave Valkey running WITHOUT a password and perfectly healthy - no render
|
||||
# error, no crash, an open server (the official image compiles protected-mode
|
||||
# OFF, so "no password" really does mean "anyone on the pod network"). The
|
||||
# init container therefore greps the mounted file for a line beginning with
|
||||
# "requirepass " and refuses to start the pod if it is absent. It never prints
|
||||
# the file.
|
||||
#
|
||||
# What this chart CANNOT cover: a change of the field's CONTENT inside the
|
||||
# vault. Helm never sees a secret materialized by the 1Password operator, so
|
||||
# there is nothing to hash. Repointing this value at a DIFFERENT item is
|
||||
# covered - checksum/secret-ref over the rendered OnePasswordItem rolls the
|
||||
# pod. After changing the password inside the item, restart by hand:
|
||||
# kubectl -n <ns> rollout restart statefulset <release>-valkey-sfs
|
||||
# (operator.1password.io/auto-restart is NOT set: on a StatefulSet it is a
|
||||
# silent no-op - see README, "Config changes must restart the workload".)
|
||||
#
|
||||
# One side effect of the probes reading the same file, stated as UNVERIFIED
|
||||
# because it has not been measured: the auth file is a directory mount, so the
|
||||
# kubelet refreshes it within about a minute of the operator writing the new
|
||||
# password, while the running server still requires the old one. The liveness
|
||||
# probe then fails and the container is restarted - i.e. a rotated password
|
||||
# probably applies itself after roughly a minute of the instance being
|
||||
# unreachable. Do not rely on it; do the rollout restart above and know when
|
||||
# the interruption happens.
|
||||
authSecretRef: null
|
||||
|
||||
valkey:
|
||||
# Replica count for this workload; 0 is allowed and scales it down. Values
|
||||
# above 1 are rejected by the schema: this chart runs ONE Valkey, and two
|
||||
# pods would fight over one ReadWriteOnce volume while clients would silently
|
||||
# get two different datasets through the Service.
|
||||
# The chart-global scaleDown flag takes precedence.
|
||||
replicas: 1
|
||||
|
||||
# Memory is the resource that decides what this chart does, because
|
||||
# valkey.maxmemory is derived from the computed memory LIMIT (see below).
|
||||
# 256Mi request x limitFactor 3 = 768Mi limit, of which Valkey may fill
|
||||
# maxmemoryFactor (0.75) = 576Mi with data.
|
||||
resources:
|
||||
# Limits are optional: memory and ephemeral-storage limits default to
|
||||
# limitFactor x the request, an explicitly set limit always wins, and a
|
||||
# CPU limit is only set when configured explicitly under "limits".
|
||||
limitFactor: 3
|
||||
requests:
|
||||
# Valkey is single-threaded for command execution; 100m is enough for a
|
||||
# per-application cache and is a request, not a cap.
|
||||
cpu: 0.1
|
||||
memory: 256Mi
|
||||
# The data volume is separate (persistence below). This covers the
|
||||
# container's own writable scratch space, which under this chart is only
|
||||
# /data when persistence is off.
|
||||
ephemeral-storage: 64Mi
|
||||
|
||||
# The fraction of the container's memory LIMIT that Valkey may fill with data.
|
||||
#
|
||||
# Why a fraction and not the whole limit: maxmemory bounds the dataset, not
|
||||
# the process. Client output buffers, replication/AOF buffers and above all
|
||||
# the copy-on-write pages of the fork() that writes an RDB snapshot or rewrites
|
||||
# the AOF all live OUTSIDE maxmemory. Handing Valkey the full limit means the
|
||||
# kubelet OOM-kills the pod at the exact moment the eviction policy was
|
||||
# supposed to take over - which is the failure this value exists to prevent.
|
||||
maxmemoryFactor: 0.75
|
||||
|
||||
# maxmemory as an explicit Kubernetes quantity (e.g. "512Mi"), overriding the
|
||||
# derivation from maxmemoryFactor. null = derive it.
|
||||
#
|
||||
# A value ABOVE the computed memory limit ABORTS THE RENDER with both numbers
|
||||
# named. It is not clamped and not warned about: a maxmemory above the limit
|
||||
# means the kubelet's OOM kill decides what the eviction policy was configured
|
||||
# to decide, and that failure arrives as a restarted pod with no explanation.
|
||||
maxmemory: null
|
||||
|
||||
# Valkey's maxmemory-policy. Default "noeviction" on purpose - see the pairs
|
||||
# at the top of this file. Under pressure Valkey then rejects writes with an
|
||||
# OOM error, which is loud in the application instead of silent in the data.
|
||||
# For a pure cache use "allkeys-lru".
|
||||
maxmemoryPolicy: noeviction
|
||||
|
||||
# Extra Valkey configuration lines, appended verbatim to the rendered
|
||||
# configuration file (e.g. "appendfsync everysec" or "timeout 300").
|
||||
#
|
||||
# The auth include is rendered AFTER this block, and in a Valkey configuration
|
||||
# the last occurrence of a directive wins - so nothing put here can switch the
|
||||
# password off, by accident or otherwise. It CAN override maxmemory and
|
||||
# maxmemory-policy, which then silently escapes the limit check above; set
|
||||
# valkey.maxmemory instead of writing a maxmemory line here.
|
||||
extraConfig: ""
|
||||
|
||||
# All three probes run the same AUTHENTICATED "valkey-cli ping" as an exec
|
||||
# probe - Valkey serves no HTTP, so there is nothing to GET. The command is a
|
||||
# script from the chart's ConfigMap (templates/valkey.configmap.yaml, key
|
||||
# ping.sh), which is where the two properties that make it an assertion rather
|
||||
# than a formality are written down and commented:
|
||||
#
|
||||
# - the answer is asserted, not just the exit code ("grep -qx PONG") AND
|
||||
# valkey-cli is called with "-e" - because without -e it EXITS 0 on an
|
||||
# error reply, so a bare "valkey-cli ping" probe would report success while
|
||||
# printing "NOAUTH Authentication required.";
|
||||
# - the password goes through the VALKEYCLI_AUTH environment variable, read
|
||||
# out of the mounted auth file, never as an "-a" argument, which would put
|
||||
# it in the process list.
|
||||
#
|
||||
# Keeping the script in the ConfigMap (instead of a one-line shell command
|
||||
# inline here) has one consequence worth knowing: editing valkey.maxmemory or
|
||||
# any other configuration value changes the ConfigMap, and checksum/config
|
||||
# then rolls the pod - which it has to do anyway for the configuration itself.
|
||||
#
|
||||
# What these probes prove: the server accepted a connection, authenticated
|
||||
# with the password from the vault, and answered PONG. What they do not prove:
|
||||
# that the dataset is intact or that the application's keys are there.
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["/bin/sh", "/etc/valkey/ping.sh"]
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["/bin/sh", "/etc/valkey/ping.sh"]
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
# The startup probe gates liveness/readiness, so those need no
|
||||
# initialDelaySeconds. Startup budget = periodSeconds x failureThreshold =
|
||||
# 150s, which is what replaying a large append-only file from disk can cost; an
|
||||
# empty instance answers on the first check.
|
||||
startupProbe:
|
||||
exec:
|
||||
command: ["/bin/sh", "/etc/valkey/ping.sh"]
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 30
|
||||
|
||||
securityContext:
|
||||
pod:
|
||||
# uid 999 / gid 1000 are NOT the repo's usual 20000/1000 and they are not
|
||||
# interchangeable: they are the "valkey" user the official image creates
|
||||
# (9.1/alpine/Dockerfile: "addgroup -S -g 1000 valkey; adduser -S -G
|
||||
# valkey -u 999 valkey" - gid 1000 because Alpine already uses gid 999).
|
||||
# The image sets no USER and starts as root; its entrypoint then steps
|
||||
# down to that user with setpriv, chown'ing /data on the way. Running the
|
||||
# pod as uid 999 directly skips that branch of the entrypoint - which is
|
||||
# the point (runAsNonRoot) and also why fsGroup has to match: without it
|
||||
# nothing would make the volume writable for the process.
|
||||
runAsNonRoot: true
|
||||
runAsUser: 999
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
fsGroupChangePolicy: Always
|
||||
# RuntimeDefault applies the container runtime's default seccomp filter.
|
||||
# It sits in the pod context on purpose: from here it covers every
|
||||
# container in the pod INCLUDING the init container that validates the
|
||||
# auth file, while a container-level profile would have to be repeated for
|
||||
# each one (and silently missed on the next one added).
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
container:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add: []
|
||||
|
||||
# Additive environment entries for the Valkey container, appended to what the
|
||||
# chart sets. Supported forms: value (rendered through tpl), secretKeyRef,
|
||||
# configMapKeyRef.
|
||||
#
|
||||
# Note what does NOT belong here: the password. The image's entrypoint appends
|
||||
# $VALKEY_EXTRA_FLAGS to the server command line, so a password routed through
|
||||
# this block would end up in the process list - exactly what the mounted
|
||||
# configuration file avoids.
|
||||
#
|
||||
# Additive means additive: an entry repeating a name the chart already sets is
|
||||
# applied at install time and LOST on an upgrade of an existing release
|
||||
# (Kubernetes merges env by name), while helm still reports success.
|
||||
env: []
|
||||
|
||||
persistence:
|
||||
# true = the dataset lives on a PVC and survives a pod restart. Off for a pure
|
||||
# cache; see the pairs at the top of this file.
|
||||
enabled: true
|
||||
|
||||
# Append-only file instead of RDB snapshots.
|
||||
#
|
||||
# Why AOF is the default: for a queue, the question is not "is there a backup"
|
||||
# but "how much is lost when the pod dies". An RDB snapshot is written every
|
||||
# few minutes, so a crash loses every job accepted since the last one - and it
|
||||
# loses them silently, because the queue comes back looking healthy, only
|
||||
# shorter. AOF appends every write, so the loss window is the fsync interval.
|
||||
# The cost is disk writes and a slower start (the file is replayed), which the
|
||||
# startup budget above accounts for.
|
||||
#
|
||||
# With persistence.enabled: false this value has nothing to append to, and the
|
||||
# chart then renders "appendonly no" plus "save ''" so that a pod without a
|
||||
# volume writes nothing to its ephemeral storage at all. NOTES.txt prints
|
||||
# which of the two is active for the release at hand.
|
||||
appendonly: true
|
||||
|
||||
# Small on purpose. On encrypted Longhorn volumes space is never returned to
|
||||
# the pool once written, so an oversized volume is paid for permanently -
|
||||
# start tight and grow (a PVC can grow; it can never shrink).
|
||||
size: 1Gi
|
||||
|
||||
# longhorn-encrypted-ns: the data here is an application's cache or queue
|
||||
# content, which regularly contains the same payloads as its database.
|
||||
# A different class (or null for the cluster default) is possible, but then
|
||||
# the data is at rest unencrypted, which is a decision and not a detail.
|
||||
storageClass: longhorn-encrypted-ns
|
||||
|
||||
networkPolicy:
|
||||
# On by default. This is a new chart with no existing installation that could
|
||||
# break on it, and a cache whose only boundary is a password is one leaked
|
||||
# credential away from being open to every pod in the cluster.
|
||||
#
|
||||
# READ THIS BEFORE RELYING ON IT: a NetworkPolicy is only a boundary if the
|
||||
# cluster's CNI enforces policies. Where it does not, the API server still
|
||||
# accepts the object and everything looks configured while nothing is
|
||||
# filtered - an unenforced policy is a false sense of security, not a weak
|
||||
# one. Verify on the cluster (try to connect from an unrelated namespace)
|
||||
# rather than reading it off this value.
|
||||
enabled: true
|
||||
|
||||
# Namespaces whose pods may reach this instance in addition to the release's
|
||||
# own namespace. Empty means: own namespace only.
|
||||
#
|
||||
# Matched by the "kubernetes.io/metadata.name" label, which Kubernetes sets on
|
||||
# every namespace itself (>= 1.21), so no labelling work is needed - but also
|
||||
# no typo protection: a namespace name that does not exist matches nothing and
|
||||
# is silently ineffective.
|
||||
allowedNamespaces: []
|
||||
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/l4rm4nd/VoucherVault/main/myapp/static/a
|
||||
|
||||
type: application
|
||||
|
||||
version: 6.1.2+up1.30.2
|
||||
version: 6.1.1+up1.30.2
|
||||
appVersion: "1.30.2"
|
||||
|
||||
maintainers:
|
||||
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "voucher-vault.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "voucher-vault.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -169,9 +167,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "voucher-vault.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 7.1.1+up4.7.0
|
||||
version: 7.1.0+up4.7.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
|
||||
@@ -19,11 +19,9 @@ Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "zipline.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. This file in template-chart is the reference
|
||||
implementation.
|
||||
NOTE: charts in this repo deliberately have no dependencies, so this helper is
|
||||
duplicated into every chart's templates/_helpers.tpl with the chart's name as
|
||||
define prefix. This file in template-chart is the reference implementation.
|
||||
*/}}
|
||||
{{- define "zipline.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
@@ -170,9 +168,8 @@ Rules:
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is duplicated into every chart's
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because sharing
|
||||
it would mean a library chart and this chart takes no dependency (README,
|
||||
guideline 14).
|
||||
templates/_helpers.tpl with the chart's name as define prefix, because charts
|
||||
in this repo deliberately have no dependencies.
|
||||
*/}}
|
||||
{{- define "zipline.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
|
||||
Reference in new issue
Block a user