Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ff54eecf7
|
||
|
|
5be10251f4
|
No files matched your search
@@ -40,6 +40,7 @@ on:
|
||||
- ytdlp-web-player
|
||||
- sure
|
||||
- valkey
|
||||
- convertx
|
||||
# freesailarr is deliberately absent: none of its ten images takes
|
||||
# its tag from appVersion (they are all pinned in its values.yaml),
|
||||
# so this workflow would rewrite appVersion and republish the chart
|
||||
|
||||
@@ -30,10 +30,9 @@ on:
|
||||
OP_CONNECT_TOKEN:
|
||||
# Needed by exactly one leg - freesailarr, for the 1Password Connect
|
||||
# lookup of its CI Wireguard key. Declared OPTIONAL on purpose, where
|
||||
# the shared publish block in workflow-util/pipelines 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"; the long form is on that repository's wiki). With
|
||||
# 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
|
||||
@@ -120,7 +119,7 @@ jobs:
|
||||
# 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.9
|
||||
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
|
||||
@@ -197,8 +196,8 @@ jobs:
|
||||
# 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 the shared
|
||||
# publish block, and it is a CI-ONLY Mullvad key: Mullvad ties an
|
||||
# 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
|
||||
@@ -217,9 +216,7 @@ jobs:
|
||||
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 the shared publish block documents at length - since
|
||||
# Gitea #66 that long form lives on the wiki of workflow-util/pipelines,
|
||||
# page "publish-helm-chart", not in this repository: v5 of
|
||||
# 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
|
||||
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-apache-tika:
|
||||
name: Publish Apache Tika Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'apache-tika'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-bentopdf:
|
||||
name: Publish BentoPDF Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'bentopdf'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-calibre-web-automated:
|
||||
name: Publish Calibre Web Automated Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'calibre-web-automated'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,15 @@
|
||||
name: Publish ConvertX Helm Chart
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- convertx/**
|
||||
jobs:
|
||||
publish-convertx:
|
||||
name: Publish ConvertX Helm Chart
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'convertx'
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-cyberchef:
|
||||
name: Publish Cyberchef Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'cyberchef'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-freesailarr:
|
||||
name: Publish Freesailarr Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'freesailarr'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-gotenberg:
|
||||
name: Publish Gotenberg Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'gotenberg'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,149 @@
|
||||
name: Publish Helm Chart with patched Application Version
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
chart-location:
|
||||
required: false
|
||||
type: string
|
||||
default: "."
|
||||
description: "location of the helm chart directory"
|
||||
chart-name:
|
||||
required: true
|
||||
type: string
|
||||
description: "name of the helm chart"
|
||||
workflow_call:
|
||||
inputs:
|
||||
chart-location:
|
||||
required: false
|
||||
type: string
|
||||
default: "."
|
||||
description: "location of the helm chart directory"
|
||||
chart-name:
|
||||
required: true
|
||||
type: string
|
||||
description: "name of the helm chart"
|
||||
outputs:
|
||||
chart-version:
|
||||
description: "the version of the published helm chart"
|
||||
value: ${{ jobs.publish-helm-chart.outputs.chart-version }}
|
||||
secrets:
|
||||
OP_CONNECT_TOKEN:
|
||||
required: true
|
||||
jobs:
|
||||
publish-helm-chart:
|
||||
outputs:
|
||||
chart-version: ${{ steps.get-chart-version.outputs.CHART_VERSION }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v7
|
||||
- name: "Install Helm"
|
||||
uses: azure/setup-helm@v5
|
||||
- name: "Configure OP-Connect"
|
||||
uses: 1password/load-secrets-action/configure@v5
|
||||
with:
|
||||
connect-host: https://op-connect.jk-dev.de
|
||||
# helm package exits 1 with "found in Chart.yaml, but missing in charts/
|
||||
# directory" while a declared dependency has not been fetched, so this
|
||||
# comes first. It is a no-op for every chart without a dependencies:
|
||||
# block; ci/helm-dependencies.sh explains why it resolves from Chart.yaml
|
||||
# instead of a committed Chart.lock. Note the consequence for the
|
||||
# published artifact: the subchart .tgz is packaged INTO ours, so the
|
||||
# version pinned in Chart.yaml is the version operators get, and nothing
|
||||
# downstream ever sees the dependency again.
|
||||
- name: "Fetch chart dependencies"
|
||||
run: ${{ github.workspace }}/ci/helm-dependencies.sh "${{ inputs.chart-name }}"
|
||||
working-directory: ${{ inputs.chart-location }}
|
||||
- name: "Package Helm Chart"
|
||||
run: helm package ${{ inputs.chart-name }}
|
||||
working-directory: ${{ inputs.chart-location }}
|
||||
- name: "Get Helm Chart Version"
|
||||
id: get-chart-version
|
||||
run: |-
|
||||
CHART_VERSION=$(grep '^version:' ${{ inputs.chart-name}}/Chart.yaml | awk '{print $2}')
|
||||
echo $CHART_VERSION
|
||||
echo "CHART_VERSION=$CHART_VERSION" >> $GITHUB_OUTPUT
|
||||
working-directory: ${{ inputs.chart-location }}
|
||||
- name: "Load Chart Registry Credentials"
|
||||
id: load-registry-creds
|
||||
uses: 1password/load-secrets-action@v5
|
||||
env:
|
||||
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||
REGISTRY_PASSWORD: op://GitHub-Actions/jlab-registry-access/password
|
||||
REGISTRY_USERNAME: op://GitHub-Actions/jlab-registry-access/username
|
||||
REGISTRY_CHARTS_URL: op://GitHub-Actions/jlab-registry-access/charts-url
|
||||
# The target is a classic, index-based Helm registry, not an OCI one, so
|
||||
# there is no "helm registry login" session and no "helm push": the upload
|
||||
# is a single HTTP POST of the packaged .tgz to <charts-url>/api/charts,
|
||||
# authenticated per request. Three properties of this step are load-bearing
|
||||
# and each of them was paid for once already.
|
||||
#
|
||||
# 1) The values are mapped into the step's own env block and read as quoted
|
||||
# shell variables - never interpolated into the command line. load-secrets-
|
||||
# action v5 (bumped in #140) no longer exports the loaded values as
|
||||
# environment variables for later steps the way v4 did; only the step
|
||||
# outputs carry them. Every "${{ env.* }}" reference therefore resolved to
|
||||
# an empty string from that bump on, with nothing reporting an error - the
|
||||
# loading step logs "Populating variable" and stays green either way. The
|
||||
# ":?" guards fail loudly and name the op:// field that is missing, and
|
||||
# quoting keeps an empty value from silently shifting arguments.
|
||||
#
|
||||
# 2) The credentials never reach the process list. "curl --user $U:$P"
|
||||
# would put them in argv; "curl --config -" reads its options from stdin
|
||||
# instead, so nothing sensitive appears in any argument. The price of that
|
||||
# config file is its value syntax: an unquoted value ends at the first
|
||||
# space (measured on curl 8.5.0 - "has space" arrived as "has"), so the
|
||||
# step refuses whitespace in the credentials up front rather than letting
|
||||
# a truncated password come back as a puzzling 401. A Gitea access token
|
||||
# is 40 hex characters, so this only ever fires on a mis-entered item.
|
||||
#
|
||||
# 3) "--fail-with-body" is what makes a failed upload fail the job. Unlike
|
||||
# "helm push", curl exits 0 on HTTP 401/403/409 unless asked not to - the
|
||||
# publish would go green while nothing was uploaded. Measured against Gitea
|
||||
# 1.24.7: a 401 without the flag is exit 0, with the flag exit 22, and the
|
||||
# response body is still printed so the log names the cause.
|
||||
#
|
||||
# The file name is built from the chart name and the version read above
|
||||
# instead of a "<chart>-*.tgz" glob: --upload-file takes exactly one file,
|
||||
# and a glob matching two turns the second into curl's URL argument, while
|
||||
# a glob matching none is passed through literally. Both fail obscurely.
|
||||
# "helm package" keeps the "+" of a "1.4.0+up1.9.2" version verbatim in the
|
||||
# file name, so "<name>-<version>.tgz" is an exact match (measured).
|
||||
- name: "Upload Helm Chart to the Chart Registry"
|
||||
env:
|
||||
REGISTRY_PASSWORD: ${{ steps.load-registry-creds.outputs.REGISTRY_PASSWORD }}
|
||||
REGISTRY_USERNAME: ${{ steps.load-registry-creds.outputs.REGISTRY_USERNAME }}
|
||||
REGISTRY_CHARTS_URL: ${{ steps.load-registry-creds.outputs.REGISTRY_CHARTS_URL }}
|
||||
CHART_NAME: ${{ inputs.chart-name }}
|
||||
CHART_VERSION: ${{ steps.get-chart-version.outputs.CHART_VERSION }}
|
||||
run: |-
|
||||
: "${REGISTRY_CHARTS_URL:?empty - check op://GitHub-Actions/jlab-registry-access/charts-url}"
|
||||
: "${REGISTRY_USERNAME:?empty - check op://GitHub-Actions/jlab-registry-access/username}"
|
||||
: "${REGISTRY_PASSWORD:?empty - check op://GitHub-Actions/jlab-registry-access/password}"
|
||||
: "${CHART_NAME:?empty - the chart-name input did not arrive}"
|
||||
: "${CHART_VERSION:?empty - could not read version: from ${CHART_NAME}/Chart.yaml}"
|
||||
# 1Password masks the full URL, but a curl connection error prints only
|
||||
# "<host>:<port>", which that mask would not cover. Mask the host too.
|
||||
CHARTS_HOST="${REGISTRY_CHARTS_URL#*://}"
|
||||
echo "::add-mask::${CHARTS_HOST%%/*}"
|
||||
case "${REGISTRY_USERNAME}${REGISTRY_PASSWORD}" in
|
||||
*[[:space:]]*)
|
||||
echo "::error::username/password in op://GitHub-Actions/jlab-registry-access must not contain whitespace - curl --config truncates an unquoted value at the first space"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
CHART_PACKAGE="${CHART_NAME}-${CHART_VERSION}.tgz"
|
||||
test -f "$CHART_PACKAGE" || {
|
||||
echo "::error::${CHART_PACKAGE} does not exist - helm package produced: $(ls -1 ./*.tgz 2>&1)"
|
||||
exit 1
|
||||
}
|
||||
echo "uploading ${CHART_PACKAGE}"
|
||||
printf 'user = %s:%s\nurl = %s\n' \
|
||||
"$REGISTRY_USERNAME" "$REGISTRY_PASSWORD" "${REGISTRY_CHARTS_URL%/}/api/charts" |
|
||||
curl --config - \
|
||||
--fail-with-body \
|
||||
--silent --show-error \
|
||||
--request POST \
|
||||
--upload-file "$CHART_PACKAGE" \
|
||||
--write-out '\nHTTP %{http_code}\n'
|
||||
working-directory: ${{ inputs.chart-location }}
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-homeassistant:
|
||||
name: Publish Homeassistant Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'homeassistant'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-matrix-synapse:
|
||||
name: Publish Matrix Synapse Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'matrix-synapse'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-minecraft-router:
|
||||
name: Publish Minecraft-Router Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'minecraft-router'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-minecraft-server:
|
||||
name: Publish Minecraft-Server Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'minecraft-server'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-outline:
|
||||
name: Publish Outline Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'outline'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-paperless-ngx:
|
||||
name: Publish Paperless-NGX Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'paperless-ngx'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-patchmon:
|
||||
name: Publish Patchmon Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'patchmon'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-rallly:
|
||||
name: Publish Rallly Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'rallly'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-rss-bridge:
|
||||
name: Publish RSS-Bridge Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'rss-bridge'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-samba:
|
||||
name: Publish Samba Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'samba'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-satisfactory:
|
||||
name: Publish Satisfactory Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'satisfactory'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-searxng:
|
||||
name: Publish SearXNG Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'searxng'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-sure:
|
||||
name: Publish Sure Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'sure'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-teamspeak-server:
|
||||
name: Publish TeamSpeak Server Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'teamspeak-server'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-urlaubsverwaltung:
|
||||
name: Publish Urlaubsverwaltung Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'urlaubsverwaltung'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-valkey:
|
||||
name: Publish Valkey Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'valkey'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-voucher-vault:
|
||||
name: Publish VoucherVault Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'voucher-vault'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-ytdlp-web-player:
|
||||
name: Publish YTDLP Web Player Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'ytdlp-web-player'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
jobs:
|
||||
publish-zipline:
|
||||
name: Publish Zipline Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'zipline'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -1,37 +0,0 @@
|
||||
name: SonarQube Analysis
|
||||
|
||||
# The analysis itself lives in workflow-util/pipelines, bound to a tag like
|
||||
# every other call to that repository. This hull only decides WHEN it runs.
|
||||
#
|
||||
# No "paths" filter, unlike the publish-<chart>.yml hulls: the analysis looks at
|
||||
# the whole tree - the five shell scripts under ci/, every workflow file, the
|
||||
# YAML and Helm templates, the values.schema.json files, and the secret
|
||||
# detection that covers everything. A filter would mean a change outside the
|
||||
# filter leaves the last result standing while claiming to be current.
|
||||
on:
|
||||
# push on main, and only main, because the SonarQube Community Build analyses
|
||||
# the main branch only - there is no pull-request analysis to hook into. The
|
||||
# consequence belongs said out loud rather than discovered: a finding appears
|
||||
# AFTER the merge. This is an observation of the state of main, not a gate,
|
||||
# and the shared block deliberately carries no quality-gate step.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
# workflow_dispatch, and it is not decoration. Three things about this path
|
||||
# are unmeasured - whether node24 is in the runner image, whether the ~59 MiB
|
||||
# scanner download gets through (a 17 MB one once did not, MTU 1500 against a
|
||||
# pod network of 1450), and whether a repository variable reaches a workflow
|
||||
# called from another repository. Any of them may need more than one attempt,
|
||||
# and without a dispatch every attempt would cost a dummy commit on main.
|
||||
# Only the maintainer can trigger it; the bot account cannot dispatch.
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sonar:
|
||||
name: SonarQube Analysis
|
||||
uses: workflow-util/pipelines/.gitea/workflows/sonar-analysis.yml@v4
|
||||
# secrets: inherit passes SONAR_TOKEN through. The block declares it as
|
||||
# required, and a missing required secret fails job CREATION on this
|
||||
# instance, not the job - the run would stand there with no steps at all.
|
||||
# SONAR_HOST_URL is a repository variable and needs nothing here.
|
||||
secrets: inherit
|
||||
@@ -85,7 +85,7 @@ helm template <chart-name> --set ingress.domain=example.com --set ingress.cluste
|
||||
|
||||
Copy `template-chart/` and replace every `xxx` placeholder — in file names (`templates/xxx.deployment.yaml` → `templates/<app>.deployment.yaml`), in `Chart.yaml`, in the `values.yaml` keys and their `.Values.xxx.*` references inside templates, and in `values.schema.json` (both the values paths and the `xxx.` helper prefixes). The schema and the null-safe fallback helpers come with the template but their content is chart-specific — adapt them along the six points of the README section "Values that must not lie", and validate the result against the chart's real bundle values, not only against its defaults. Then wire up CI:
|
||||
|
||||
1. Add `.github/workflows/publish-<chart-name>.yml` (copy an existing one, e.g. `publish-outline.yml`) — it triggers on pushes to `main` touching `<chart-name>/**` and calls the reusable `publish-helm-chart.yml`, which lives in `workflow-util/pipelines` and is bound to a tag. Copying an existing hull gets the `uses:` path, `registry-owner: ${{ github.repository_owner }}` and `secrets: inherit` right; writing one from memory does not.
|
||||
1. Add `.github/workflows/publish-<chart-name>.yml` (copy an existing one, e.g. `publish-outline.yml`) — it triggers on pushes to `main` touching `<chart-name>/**` and calls the reusable `publish-helm-chart.yml`.
|
||||
2. Add the chart name to the `chart-name` choice list in `.github/workflows/change-app-version.yml`.
|
||||
3. Add install-test fixtures under `ci/<chart-name>/` (`test-values.yaml`, `fixtures.yaml`) or an entry with justification in `ci/excluded-charts.txt` — the PR install-test fails if both are missing.
|
||||
4. Add a `customManagers` block for the chart's main image in `renovate.json5` (one per chart, since the image repository lives in the template and the version in `Chart.yaml`) — without it Renovate never updates that chart's app version. See README section "Dependency updates (Renovate)".
|
||||
|
||||
@@ -17,6 +17,7 @@ Every chart has a wiki page of its own — `https://git.jk-dev.de/jlab-cluster/h
|
||||
| `apache-tika` | Apache Tika | Deployment + Service only (no ingress) |
|
||||
| `bentopdf` | BentoPDF | Stateless Deployment + `config.json` ConfigMap, `subPath`-mounted into the web root and always rendered (empty renders as `{}`) |
|
||||
| `calibre-web-automated` | Calibre-Web Automated | StatefulSet, extra consume-PVC |
|
||||
| `convertx` | ConvertX | StatefulSet + PVC at `/app/data` (SQLite, uploads and converted output in one volume), no external database. **The only chart that overrides the image's `command` to bypass its entrypoint**: the image carries no `USER` and its entrypoint exists solely to drop to uid 1000 with `gosu`, so the chart starts the app directly and lets `runAsUser`/`fsGroup` do that work instead — the hardening baseline stays intact, at the price that any logic upstream later moves into that entrypoint is skipped silently (re-read it at every app-version update; the reasoning sits at the `command:` line). Pins uid 1000 rather than the repo's 20000, because the image owns `/home/convertx` with it and the uid owns PV data. `readOnlyRootFilesystem: true` with two emptyDirs, `/tmp` and `/home/convertx` — the latter is `HOME`, which the chart also sets explicitly, because LibreOffice, Calibre and fontconfig build their profiles there on the first conversion. Probes use `/healthcheck` (static 200, unauthenticated); `/` is never a 200, it 302s to `/setup` or `/login`. `JWT_SECRET` is a required `OnePasswordItem` — unset, the app mints a fresh one per process and every restart logs everyone out. The ingress `proxy-body-size` annotation is the **only** upload ceiling, since the app enforces none. ~1.4 GiB image, hence a raised `INSTALL_TIMEOUT` |
|
||||
| `cyberchef` | CyberChef | Stateless Deployment |
|
||||
| `freesailarr` | gluetun VPN media stack (qbittorrent, prowlarr, flaresolverr, radarr, recyclarr, sonarr, seerr, bazarr, and mediathekarr off by default) | Single-pod StatefulSet: all apps share gluetun's network namespace; config volumes via volumeClaimTemplates, media PVCs as existingClaims (optional per-category `subPath`, so one claim can serve all three categories). One ClusterIP Service per app (mediathekarr's carries both of its ports); no global `ingress.domain`. Ingress in two shapes that mix: one host per app (default, each app's host required), or one shared `ingress.host` with the apps as sub-paths (documented ingress exception below) |
|
||||
| `gotenberg` | Gotenberg | Deployment + Service only (no ingress) |
|
||||
@@ -51,30 +52,23 @@ Every chart has a wiki page of its own — `https://git.jk-dev.de/jlab-cluster/h
|
||||
## CI/CD
|
||||
|
||||
- `publish-<chart>.yml` — one thin workflow per chart; triggers on pushes to `main` touching `<chart>/**` (or manually) and calls the reusable workflow below. **A push to a chart directory on `main` publishes that chart**, so the chart version must be bumped in the same change.
|
||||
- `publish-helm-chart.yml` — **not in this repository any more.** The reusable workflow lives in [`workflow-util/pipelines`](https://git.jk-dev.de/workflow-util/pipelines), the shared building-block repo, and the callers here bind it to a tag: `uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3` (Gitea #66). It still does the same thing — `helm package`, then a single HTTP `POST` of the packaged `.tgz` to the private chart registry, which is a **classic, index-based** Helm registry and not an OCI one, so `helm push` is not used, there is no `helm registry login` session, and every request carries its own Basic auth. What changed is who supplies what:
|
||||
- **`registry-owner: ${{ github.repository_owner }}`** in each caller's `with:`. The owner used to be derived inside the workflow (#63); it is now a required input with no default, so the expression is evaluated **here**, in the calling repository, where the value is measured. That is `pipelines`' house rule — a shared block never guesses an owner segment — and it is why every caller carries that one extra line.
|
||||
- **`secrets: inherit`**, unchanged, for `OP_CONNECT_TOKEN`.
|
||||
- the repository **variable `OP_CONNECT_HOST`**, which replaced the Connect host that used to be hardcoded in our copy. A shared block must not carry one tenant's infrastructure address; the block's first step fails and names the variable when it is unset.
|
||||
- `publish-helm-chart.yml` — reusable workflow: `helm package`, then a single HTTP `POST` of the packaged `.tgz` to the private chart registry. The target is a **classic, index-based** Helm registry (Gitea's `/api/charts` endpoint), not an OCI one: `helm push` speaks only OCI and is not used, there is no `helm registry login` session, and every request carries its own Basic auth. Credentials **and the target URL** come from 1Password Connect (`OP_CONNECT_TOKEN` secret, item `op://GitHub-Actions/jlab-registry-access/` with `username`, `password`, `charts-url`); the workflow appends `/api/charts` to `charts-url`. No registry address ever appears in this repository. The loaded values are read via **`steps.<id>.outputs.*`**, mapped into the step's own `env:` block and used as quoted shell variables — never interpolated into the command line. `load-secrets-action` **v5 no longer exports them as environment variables** for later steps the way v4 did, so after the v5 bump every `${{ env.* }}` reference resolved to an empty string while the loading step still logged `Populating variable` and stayed green. Each value is additionally guarded with `:?`, because an empty one used to collapse into a missing argument rather than an error. Three properties of the upload step are load-bearing:
|
||||
- **Credentials stay out of the process list.** `curl --user "$U:$P"` would put them in `argv`; the step uses `curl --config -` and pipes the options in on stdin instead.
|
||||
- **A failed upload fails the job.** Unlike `helm push`, `curl` exits **0** on HTTP 401/403/409 — a publish would go green with nothing uploaded. `--fail-with-body` turns those into a non-zero exit and still prints the response body, so the log names the cause.
|
||||
- **The file name is built, not globbed.** `<chart>-<version>.tgz` is assembled from the chart name and the version already read from `Chart.yaml`; `--upload-file` takes exactly one file, and a glob matching two silently turns the second into curl's URL argument. `helm package` keeps the `+` of a `1.4.0+up1.9.2` version verbatim in the file name, so the built name is an exact match.
|
||||
|
||||
The registry credentials and the target URL still come from 1Password Connect (item `op://GitHub-Actions/jlab-registry-access/` with `username`, `password`, `charts-url`). `charts-url` holds only the **base up to `/api/packages`** and the block composes `<charts-url>/<owner>/helm/api/charts`, so **no registry address appears in this repository** — not even the owner path segment. `ci/helm-dependencies.sh` is still run before `helm package`, and it is still **our** script: the block's checkout clones the calling repository, so it runs from this tree.
|
||||
|
||||
The reasons behind the upload step are **not repeated here**, because they are no longer ours to keep current: why both halves of the composed URL are guarded, why the credentials go in through `curl --config -` instead of `--user`, why `--fail-with-body` is what makes a failed upload fail the job, and why the file name is built rather than globbed, are all on the [`publish-helm-chart` wiki page](https://git.jk-dev.de/workflow-util/pipelines/wiki/publish-helm-chart) of that repository. Nothing was dropped in the move — the upload step's shell body went across unchanged except for two error messages that name the owner as an input.
|
||||
|
||||
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. That is also what makes a manual `workflow_dispatch` of any `publish-<chart>.yml` a harmless way to prove the shared block works.
|
||||
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 the shared publish block (which is why that block names this script as a prerequisite of its callers — see above), 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.
|
||||
- `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.
|
||||
|
||||
**It stays in this repository, and that is a decision, not an oversight** (Gitea #66, decision 4). It is reusable in form but not in substance: it is welded to the `ci/` fixtures, to the `ci/excluded-charts.txt` parser and to this repository's chart matrix. A building block that only one repository can use is not shared, it is merely moved somewhere else — and moving it would buy nothing while putting a tag between us and a file we change often. `publish-helm-chart.yml` was the opposite case: it takes a chart name and a registry owner and knows nothing about this repository, which is exactly why it could go. This note exists so the question is not asked again in half a year.
|
||||
- `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.
|
||||
- `sonar.yml` — **SonarQube analysis**, a hull like the publish ones: it triggers on pushes to `main` (no `paths` filter — the analysis looks at the whole tree) or manually, and calls `workflow-util/pipelines/.gitea/workflows/sonar-analysis.yml@v4` with `secrets: inherit`. The project-specific half is `sonar-project.properties` in the repo root. **It is not a gate** and it does not block anything — 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 25 thin `publish-<chart>.yml` hulls run unchanged — including now that the workflow they call lives in another repository (Gitea #66). 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 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.
|
||||
|
||||
**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`.
|
||||
|
||||
@@ -151,7 +145,7 @@ Fixtures live at the **repo root** under `ci/` (deliberately *not* inside the ch
|
||||
- `ci/<chart>/settings.env` — optional overrides: `INSTALL_TIMEOUT` for slow starters, and `REQUIRED_ENV="VAR_A VAR_B"` to declare the variables `fixtures.env.yaml` needs. **A missing or empty declared variable fails the install-test loudly, naming the variable — it never skips the chart.** Skipping would restore exactly the green-but-meaningless signal this mechanism exists to remove; the one legitimate case of absent secrets, a PR from a fork, belongs in an `if` on the workflow job rather than in the script
|
||||
- `ci/excluded-charts.txt` — charts that cannot run in CI, one per line as `<chart> <justification>`. Currently: `satisfactory` (multi-GB image + 10GB+ Steam download), `template-chart` (scaffold with placeholder image). The justification is mandatory: a line carrying only a chart name is deliberately **not** an exclusion, because a bare name would drop a chart out of CI with nothing to review. **An excluded chart gets no install-test job at all** — see below
|
||||
|
||||
`freesailarr` is the only chart using `fixtures.env.yaml` today: gluetun needs a real Wireguard key, or it never completes a handshake and no app container behind its kill switch becomes Ready. The install-test job loads the key from 1Password Connect for that chart only, using the same pattern as the shared publish block — `steps.<id>.outputs.*` into the step's own `env:`, quoted, with `:?` guards that name the missing `op://` field before the cluster is even created. The key in that item is a **CI-only Mullvad key**, deliberately not the production one: Mullvad ties an address to a key and routes the tunnel to the endpoint it saw last, so a shared key would make 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.
|
||||
`freesailarr` is the only chart using `fixtures.env.yaml` today: gluetun needs a real Wireguard key, or it never completes a handshake and no app container behind its kill switch becomes Ready. The install-test job loads the key from 1Password Connect for that chart only, using the same pattern as `publish-helm-chart.yml` — `steps.<id>.outputs.*` into the step's own `env:`, quoted, with `:?` guards that name the missing `op://` field before the cluster is even created. The key in that item is a **CI-only Mullvad key**, deliberately not the production one: Mullvad ties an address to a key and routes the tunnel to the endpoint it saw last, so a shared key would make 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.
|
||||
|
||||
**An excluded chart is dropped from the matrix, not skipped inside its job (#231).** It used to get a job that ran the script, printed `SKIP` and exited 0 — and GitHub knows only the exit code, so the check list showed `Install-test satisfactory ✓` for a chart whose image was never pulled, indistinguishable from one that really came up in the cluster. `detect-changed-charts` now removes the excluded charts before the matrix is built, so no job exists for them and nothing claims they passed. Because an absence on its own says nothing, that job writes every excluded chart of the PR **with its documented justification** into the job summary — a `Skipped` would only say "not run", and why is the question a review actually asks. The list is parsed in exactly one place: `ci/run-install-test.sh --list-excluded` emits `<chart><TAB><reason>` and the workflow calls it instead of re-implementing the format in YAML, since a second parser would be a second chance to select different charts — silently, and in either direction. The script keeps its own `SKIP` branch for local runs. What cannot be done instead: deciding per chart in an `if` on the install-test job, to get a visible `Skipped` entry rather than an absent one. GitHub rejects it with `context "matrix" is not allowed here` — `jobs.<id>.if` is evaluated **before** the matrix is expanded, so the `matrix` context does not exist there, and the whole workflow ends in failure with zero jobs created (measured on the closed PR #233).
|
||||
|
||||
@@ -170,32 +164,7 @@ Why it exists at all: the answer arrived once by accident. Gitea PR #48 correcte
|
||||
- **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.
|
||||
|
||||
### SonarQube analysis (after the merge, and not a gate)
|
||||
|
||||
`sonar.yml` sends the whole tree through a SonarQube analysis. The workflow itself is the shared block [`sonar-analysis.yml`](https://git.jk-dev.de/workflow-util/pipelines/wiki/sonar-analysis) in `workflow-util/pipelines`, bound to `@v4`; the only thing on this side is `sonar-project.properties` in the repo root, holding the project key `JLab-Helm-Charts`. **That key is read off the existing project in SonarQube, not derived** — it is the one the maintainer set up, and a guessed key (`jlab-cluster_helm-charts`, reasoned from the instance serving two owners) is what made the first run fail. The file carries **no `sonar.projectName`** on purpose: once the project exists, the display name belongs to the server, and a name sent with every analysis would overwrite a rename there silently.
|
||||
|
||||
**What is analysed — measured, not taken from a language list.** The first run (run 375) loaded exactly **two quality profiles, `json` and `yaml`**; the sensors that did work were **IaC Kubernetes** (3.4 s) and the **secrets detection** (`TextAndSecretsSensor`) over all 391 indexed files.
|
||||
|
||||
| What is in this repository | Analyser | Runs? |
|
||||
|---|---|---|
|
||||
| 238 YAML files and 26 Helm template directories | IaC Kubernetes, YAML | yes |
|
||||
| 26 `values.schema.json` | JSON | yes |
|
||||
| the whole tree (391 files) | Secrets | yes |
|
||||
| the 5 shell scripts under `ci/` — the whole CI logic | Shell | **no** |
|
||||
| the 30 workflow files | GitHub Actions | **no** |
|
||||
|
||||
**The two missing rows are not a configuration mistake, and nothing in this repository can fix them: analysers come from the *server*, not from the scanner.** The language list the earlier version of this table was built from describes the newest SonarQube release; this instance does not have the Shell and GitHub Actions plugins. The consequence is worth stating plainly rather than leaving it to be rediscovered — **the files carrying the actual logic, the five `ci/` scripts and the thirty workflows, are not analysed at all.** What runs is IaC over the charts plus secret detection over everything. Whether the instance gets upgraded or gains those plugins is a decision for whoever runs it, not something this repository settles; `shellcheck` and review remain the only check on the scripts until then.
|
||||
|
||||
The secrets row is the valuable one that *does* run. **The premise of this repository is that no credential ever lands in it** — every secret is a 1Password `OnePasswordItem` reference — and until now nothing checked that premise. What it actually reports is still unknown: run 375 built the report (603 kB) but never uploaded it, because the upload is where the wrong project key failed.
|
||||
|
||||
**It is not a gate, and that is a decision.** The SonarQube Community Build analyses the **main branch only**; there is no pull-request analysis to hook into, so the trigger is a push to `main` and **a finding appears after the merge**. At that point there is nothing left to block, which is why the shared block carries no quality-gate step: a red gate behind the merge would only colour `main` red without having prevented anything, and a permanent red is a thing people learn to ignore — taking the credibility of `validate-charts` with it. A gate becomes the right thing under one of two conditions: pull-request analysis is available, or a deployment hangs off the workflow. Until then, read the findings in SonarQube; nothing in this repository turns red because of them.
|
||||
|
||||
**No exclusions, for the first round.** Including the Helm templates, which are not valid YAML because of `{{ }}`. What the Helm and YAML analysers make of them is only learnable by letting them run over them once, and excluding them up front would discard that answer along with the bulk of the repository. If the first run produces noise genuinely not worth reading, the exclusions go into `sonar-project.properties` with the finding they answer named next to them — not before.
|
||||
|
||||
**Prerequisites**, both on the instance and neither in a file here: the repository **variable `SONAR_HOST_URL`** (a variable and not a secret on purpose — the server address is not a credential, and as a variable it stays readable in the log, which is what makes a wrong one diagnosable) and the **secret `SONAR_TOKEN`**, which reaches the block through `secrets: inherit`. The block guards both and names the setting and its place when one is empty. A *missing* `SONAR_TOKEN` never gets that far: the block declares it `required`, and a missing required secret fails job **creation** on this instance, so the run would show no steps at all — the same quirk as everywhere else here.
|
||||
|
||||
**Three things about this path were unmeasured, and run 375 answered all three — positively.** `node24` **is** in the runner image, so the JavaScript scan action runs. The scanner download **gets through**: ~59 MiB of archive plus its signature in roughly three seconds, so the MTU failure that once killed a 17 MB download on this runner (see "What a GitHub runner had been providing quietly") does not repeat at five times the size. And a repository **variable does reach a workflow called from another repository** — the block's guard passed and the scanner started, which it could not have done without `vars.SONAR_HOST_URL` arriving. That last one also settles the identical question the publish route carried for `OP_CONNECT_HOST`: same forge version, same mechanism, a repository variable read inside a workflow called from another repository. What the publish route still has unproven is the upload itself, which has never run. `workflow_dispatch` stays on the hull for the retries: without it every attempt costs a dummy commit on `main`.
|
||||
- **A big image does not move that figure as much as it looks.** `convertx` was expected to, at ~1.4 GiB compressed — by a wide margin the largest image in the repo, and the reason its `settings.env` raises `INSTALL_TIMEOUT` to 900s. Measured on its first green run (Gitea PR #61) the whole leg took **2m35s**, cluster creation and rollout wait included, so it sits inside the existing envelope rather than setting a new one. Worth knowing before the next raised timeout is read as a prediction about the total: the legs run concurrently, so what the all-charts figure tracks is the **slowest** leg plus scheduling, not the sum of the pulls.
|
||||
|
||||
## Dependency updates (Renovate)
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Fixtures for the convertx install-test: the Secret the 1Password operator
|
||||
# would otherwise materialize from the OnePasswordItem (release name: ci).
|
||||
#
|
||||
# Without it the pod stays in CreateContainerConfigError - the StatefulSet
|
||||
# reads JWT_SECRET from this Secret's "jwt-secret" key, and the stub
|
||||
# OnePasswordItem CRD in ci/common/ has no controller behind it. The key name
|
||||
# here is therefore load-bearing and has to match the chart's secretKeyRef.
|
||||
#
|
||||
# ConvertX needs no external database, cache or storage service: its SQLite
|
||||
# file, uploads and output all live on the chart's own PVC, so this is the
|
||||
# whole fixture set.
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ci-convertx-secret
|
||||
stringData:
|
||||
jwt-secret: convertx-ci-jwt-secret-0123456789abcdef0123456789abcdef
|
||||
@@ -0,0 +1,23 @@
|
||||
# The image is ~1.4 GiB compressed - by a wide margin the largest in this
|
||||
# repo, because LibreOffice, Calibre, ImageMagick, ffmpeg, Inkscape and a TeX
|
||||
# Live installation are all inside it. One layer alone is 1354 MiB.
|
||||
#
|
||||
# The rollout wait therefore has to cover the PULL, not just the start: the
|
||||
# runner reference point is 96 MB in 17.5s, which puts this image in the
|
||||
# region of five minutes of pulling before the container is even created. On
|
||||
# top of that comes the startup probe budget (30 x 10s = 300s), which is
|
||||
# generous because ConvertX shells out to ~20 converter binaries at import
|
||||
# just to log their versions.
|
||||
#
|
||||
# MEASURED, and the estimate was wrong in the safe direction: the first green
|
||||
# install-test of this chart (Gitea PR #61) took 2m35s for the WHOLE job -
|
||||
# cluster creation, fixtures, pull, install and rollout wait together. The
|
||||
# 96 MB/17.5s reference does not transfer to ghcr.io on this runner.
|
||||
#
|
||||
# 900s stays anyway, and deliberately. A rollout-wait timeout is a ceiling,
|
||||
# not a cost: kubectl waits exactly as long as the rollout needs, so a
|
||||
# generous number is free on a good day and only spends time on a chart that
|
||||
# is already failing. What it buys is the bad day - a cold registry, or a node
|
||||
# that has to extract the 1354 MiB layer onto slow disk. The default of 300s
|
||||
# would have no headroom at all for either.
|
||||
INSTALL_TIMEOUT=900
|
||||
@@ -0,0 +1,15 @@
|
||||
# The OnePasswordItem CR only needs a non-null item path; the actual
|
||||
# Kubernetes Secret comes from ci/convertx/fixtures.yaml.
|
||||
secrets:
|
||||
jwtSecretRef: op://ci/convertx-jwt
|
||||
|
||||
ingress:
|
||||
domain: convertx.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
|
||||
|
||||
# The chart's default is 10Gi; the install-test converts nothing, so 1Gi is
|
||||
# enough and provisions faster on k3d's local-path class.
|
||||
storage:
|
||||
amount: 1Gi
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: v2
|
||||
name: convertx
|
||||
description: A Helm chart to deploy ConvertX on Kubernetes
|
||||
icon: https://raw.githubusercontent.com/C4illin/ConvertX/main/public/android-chrome-512x512.png
|
||||
|
||||
type: application
|
||||
|
||||
version: 1.0.0+upv0.19.0
|
||||
appVersion: "v0.19.0"
|
||||
|
||||
maintainers:
|
||||
- name: jklein
|
||||
@@ -0,0 +1,224 @@
|
||||
{{- $replicas := include "convertx.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.convertx.replicas) -}}
|
||||
{{- $startup := fromYaml (include "convertx.startupProbe" .Values.convertx.startupProbe) -}}
|
||||
{{- $autoDelete := include "convertx.autoDeleteEveryNHours" .Values.convertx.autoDeleteEveryNHours -}}
|
||||
{{- $registration := include "convertx.boolString" (dict "value" .Values.convertx.accountRegistration "default" false "path" "convertx.accountRegistration") -}}
|
||||
{{- $unauthenticated := include "convertx.boolString" (dict "value" .Values.convertx.allowUnauthenticated "default" false "path" "convertx.allowUnauthenticated") -}}
|
||||
{{- $httpAllowed := include "convertx.boolString" (dict "value" .Values.convertx.httpAllowed "default" false "path" "convertx.httpAllowed") -}}
|
||||
ConvertX {{ .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
|
||||
{{- else }}
|
||||
|
||||
ACCESS — NO INGRESS
|
||||
ingress.enabled is false, so this release has no Ingress and no certificate,
|
||||
and neither ingress.clusterIssuer nor ingress.domain is needed. ConvertX has
|
||||
no base-URL setting of its own — it builds no absolute links — so nothing
|
||||
outside the Ingress reads the domain in this chart.
|
||||
|
||||
Reach the release inside the cluster with
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80
|
||||
|
||||
and open http://localhost:8080. Note that the session cookie is marked
|
||||
"secure"{{ if eq $httpAllowed "true" }} — except that convertx.httpAllowed is true in this release, which
|
||||
clears that flag, so logging in over plain http works{{ else }}, so logging in over plain http will NOT work: the browser
|
||||
refuses to store the cookie. convertx.httpAllowed: true clears that flag{{ end }}.
|
||||
{{- end }}
|
||||
{{ if eq $replicas "0" }}
|
||||
SCALED TO ZERO
|
||||
{{ if .Values.scaleDown }}scaleDown: true{{ else }}convertx.replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so the endpoint
|
||||
above does not answer until you set it back and upgrade again. PVC, Secret{{ if .Values.ingress.enabled }},
|
||||
Service and Ingress stay in place, so neither the conversions nor the
|
||||
certificate are touched.{{ else }} and
|
||||
Service stay in place, so the conversions on the PVC are untouched.{{ end }}
|
||||
{{ end }}
|
||||
WHOEVER OPENS THE PAGE FIRST BECOMES THE ADMINISTRATOR
|
||||
A fresh ConvertX has no users, and with no users it serves a SETUP FORM to
|
||||
every visitor — no password, no invitation, nothing to bypass. The first
|
||||
account created that way owns the instance, and convertx.accountRegistration
|
||||
does not gate it: that value decides whether accounts TWO and onwards can be
|
||||
created, not the first one.
|
||||
|
||||
So on a fresh install, register yourself before the DNS name is reachable by
|
||||
anyone else. There is no second chance and no "reset admin" command; the
|
||||
repair is deleting the users row from the SQLite database on the PVC.
|
||||
{{- if eq $unauthenticated "true" }}
|
||||
|
||||
This release has convertx.allowUnauthenticated: true, so the point above is
|
||||
moot in the worst way — see the warning further down.
|
||||
{{- end }}
|
||||
|
||||
THE 1PASSWORD ITEM MUST CARRY THE FIELD THE CHART READS
|
||||
secrets.jwtSecretRef points at
|
||||
{{ .Values.secrets.jwtSecretRef }}
|
||||
The 1Password operator materializes that item as the Kubernetes Secret
|
||||
{{ .Release.Name }}-{{ .Chart.Name }}-secret, with one Secret key per FIELD of the item. So the field
|
||||
name is not free-form: this chart reads exactly one field, and it must be
|
||||
spelled
|
||||
|
||||
jwt-secret a long random string — openssl rand -hex 32
|
||||
|
||||
An item without it does not fail the install. The release goes through and
|
||||
the pod then hangs in CreateContainerConfigError naming the key it could not
|
||||
find. 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
|
||||
|
||||
WHY THIS VALUE IS REQUIRED RATHER THAN OPTIONAL: ConvertX signs its session
|
||||
cookies with JWT_SECRET, and when the variable is unset it quietly generates
|
||||
a random one PER PROCESS. Every restart — a node drain, an image update, an
|
||||
OOM kill — would then invalidate every session, with no log line and no
|
||||
error. Rotating the field has the same effect on purpose; after a rotation:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} rollout restart statefulset/{{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
|
||||
Editing the value INSIDE the item does not roll the pod by itself (Helm
|
||||
never sees a secret's content). Pointing secrets.jwtSecretRef at a DIFFERENT
|
||||
item does — the pod template carries a checksum over the rendered
|
||||
OnePasswordItem.
|
||||
|
||||
TWO SWITCHES THAT OPEN THE INSTANCE UP
|
||||
convertx.accountRegistration {{ $registration }}
|
||||
convertx.allowUnauthenticated {{ $unauthenticated }}
|
||||
|
||||
Upstream warns explicitly against enabling either on an instance reachable
|
||||
from the internet, and this chart puts a public DNS name in front by
|
||||
default. accountRegistration lets anyone with the URL create an account;
|
||||
allowUnauthenticated removes the login entirely, so anyone who reaches the
|
||||
Service can upload files and read every conversion on the volume.
|
||||
{{- if or (eq $registration "true") (eq $unauthenticated "true") }}
|
||||
|
||||
*** {{ if and (eq $registration "true") (eq $unauthenticated "true") }}BOTH OF THEM ARE ON{{ else }}ONE OF THEM IS ON{{ end }} IN THIS RELEASE. ***
|
||||
{{- if eq $unauthenticated "true" }}
|
||||
convertx.allowUnauthenticated: true — there is no login. Anyone who can
|
||||
reach {{ if .Values.ingress.enabled }}https://{{ .Values.ingress.domain }}{{ else }}the Service{{ end }} can use it and read other people's files.
|
||||
{{- end }}
|
||||
{{- if eq $registration "true" }}
|
||||
convertx.accountRegistration: true — anyone with the URL can sign up.
|
||||
{{- end }}
|
||||
If this instance is reachable from the internet, that is almost certainly
|
||||
not what you wanted.
|
||||
{{- end }}
|
||||
|
||||
RETENTION IS A VALUE, AND IT IS THE ONLY THING THAT EMPTIES THE VOLUME
|
||||
{{- if eq $autoDelete "0" }}
|
||||
convertx.autoDeleteEveryNHours is 0 — THE CLEANUP IS OFF. Uploads and
|
||||
converted output accumulate on the PVC for as long as the release lives and
|
||||
nothing reclaims them. Watch the volume, or set a non-zero number of hours.
|
||||
{{- else }}
|
||||
convertx.autoDeleteEveryNHours is {{ $autoDelete }} — ConvertX deletes the uploads and the
|
||||
output of any job older than {{ $autoDelete }} hours. That is a hard delete, not an
|
||||
archive: a conversion nobody downloaded in time is gone. Set it to 0 to
|
||||
switch the cleanup off, and then size storage.amount for unbounded growth.
|
||||
{{- end }}
|
||||
|
||||
STORAGE
|
||||
{{ .Release.Name }}-{{ .Chart.Name }}-pvc, {{ .Values.storage.amount }}, {{ if .Values.storage.storageClass }}storage class "{{ .Values.storage.storageClass }}"{{ else }}on the cluster's default storage class
|
||||
(storage.storageClass is unset){{ end }}, mounted at /app/data.
|
||||
|
||||
Everything this release persists is on that one volume: the SQLite database
|
||||
(users, jobs, file names), every uploaded file and every converted result.
|
||||
There is no external database and no object storage, so one snapshot of the
|
||||
PVC is a complete backup — and losing it loses the accounts along with the
|
||||
files.
|
||||
|
||||
Because the PVC is rendered by the chart, helm uninstall deletes it and the
|
||||
data with it, unless the storage class reclaim policy says otherwise.
|
||||
{{- if gt (int $replicas) 1 }}
|
||||
|
||||
convertx.replicas is {{ $replicas }}, and that does not work. The PVC is ReadWriteOnce, so
|
||||
the extra pods normally stay Pending on "Multi-Attach error for volume"; on
|
||||
a genuinely RWX storage class they would start and then corrupt the SQLite
|
||||
database two processes are now writing. Set it back to 1.
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.enabled }}
|
||||
|
||||
THE INGRESS IS THE ONLY UPLOAD CEILING
|
||||
ingress.maxUploadFileSize {{ .Values.ingress.maxUploadFileSize }}
|
||||
|
||||
ConvertX enforces no size limit of its own — none, at any setting — so this
|
||||
annotation is the whole story. A file above it never reaches the
|
||||
application: the user gets the ingress controller's 413 page and nothing
|
||||
appears in the pod log, which makes it look like a broken upload button
|
||||
rather than a limit.
|
||||
|
||||
Raising it has a second cost: the upload lands on the PVC above, so a
|
||||
ceiling far beyond storage.amount just moves the failure from a 413 to a
|
||||
full volume.
|
||||
{{- else }}
|
||||
|
||||
UPLOAD CEILING — NONE FROM THIS CHART
|
||||
ingress.enabled is false, so the chart sets no body-size limit anywhere, and
|
||||
ConvertX enforces none of its own. Whatever proxy you put in front brings
|
||||
its ceiling with it — ingress-nginx defaults to 1m, small enough to look
|
||||
like a broken upload button.
|
||||
{{- end }}
|
||||
|
||||
WHAT A GREEN POD PROVES
|
||||
All three probes are HTTP GETs against /healthcheck, which ConvertX answers
|
||||
with a static 200 and no authentication. It touches neither the database nor
|
||||
the volume, so a green pod proves the process is serving — not that a
|
||||
conversion works and not that the volume is writable.
|
||||
|
||||
It is deliberately NOT "/": with the defaults of this chart "/" answers 302,
|
||||
to /setup while no account exists and to /login afterwards. A probe would
|
||||
pass on that redirect, and pass on a different one from day two onwards.
|
||||
|
||||
The startup probe grants {{ $startup.failureThreshold }} × {{ $startup.periodSeconds }}s = {{ mul $startup.failureThreshold $startup.periodSeconds }}s. That budget is generous because
|
||||
ConvertX shells out to about twenty converter binaries at startup just to
|
||||
log their versions.
|
||||
|
||||
THE FIRST PULL IS SLOW, AND THAT IS NORMAL
|
||||
The image is roughly 1.4 GiB compressed — LibreOffice, Calibre, ImageMagick,
|
||||
ffmpeg, Inkscape and a TeX Live installation are all inside it. On a node
|
||||
that has not seen it before, expect several minutes in ContainerCreating
|
||||
before the startup probe budget above even begins. A pod that looks stuck
|
||||
there is usually still pulling:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} describe pod {{ .Release.Name }}-{{ .Chart.Name }}-sfs-0
|
||||
|
||||
THIS CHART BYPASSES THE IMAGE ENTRYPOINT
|
||||
The image runs as root and drops privileges itself; this chart starts the
|
||||
application directly (command: bun run dist/src/index.js) as uid 1000, with
|
||||
fsGroup doing what the entrypoint's chown would have done. PUID, PGID and
|
||||
UMASK therefore configure NOTHING here — setting them through convertx.env
|
||||
has no effect at all, and the uid belongs in convertx.securityContext.pod.
|
||||
|
||||
The cost to know about: if upstream moves real work into that entrypoint,
|
||||
this chart skips it silently. Re-read the upstream entrypoint.sh at every
|
||||
app-version update.
|
||||
{{- if .Values.convertx.env }}
|
||||
|
||||
EXTRA ENVIRONMENT VARIABLES ARE ACTIVE
|
||||
convertx.env adds {{ len .Values.convertx.env }} variable(s), rendered after the ones the chart sets
|
||||
itself. Two warnings that go with that block:
|
||||
|
||||
- WEBROOT moves every route of the application, /healthcheck included, so
|
||||
it breaks all three probes while the app is perfectly healthy — and this
|
||||
chart's Ingress still maps "/". Do not set it here.
|
||||
- An entry repeating a name the chart already sets is additive, not a
|
||||
replacement. At install yours wins; on an EXISTING release Kubernetes
|
||||
merges env by name and the chart's own value survives instead, while helm
|
||||
upgrade still reports success. Read it back off the StatefulSet.
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
|
||||
EXTRA INGRESS ANNOTATIONS ARE ACTIVE
|
||||
{{- range $key, $value := . }}
|
||||
{{ $key }}: {{ $value }}
|
||||
{{- end }}
|
||||
These are merged with the cert-manager and proxy-body-size annotations the
|
||||
chart always sets. Annotations are interpreted by the ingress controller, so
|
||||
a misspelled key is silently ignored — it does not fail the render.
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,327 @@
|
||||
{{/*
|
||||
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. template-chart/templates/_helpers.tpl is the reference implementation.
|
||||
*/}}
|
||||
{{- define "convertx.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 "convertx.resourceKey" $key) $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $limits := dict -}}
|
||||
{{- range $key, $value := ($resources.limits | default dict) -}}
|
||||
{{- if not (kindIs "invalid" $value) -}}
|
||||
{{- $_ := set $limits (include "convertx.resourceKey" $key) $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- range $key, $request := $requests -}}
|
||||
{{- if and (ne $key "cpu") (not (hasKey $limits $key)) -}}
|
||||
{{- $_ := set $limits $key (include "convertx.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 "convertx.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 "convertx.multiplyQuantity" -}}
|
||||
{{- $quantity := include "convertx.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 "convertx.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 "convertx.formatNumber" -}}
|
||||
{{- if kindIs "float64" . -}}
|
||||
{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%v" . -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render the replica count for the StatefulSet.
|
||||
|
||||
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 "convertx.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.
|
||||
This helper defines the opposite semantics: an empty block means "chart
|
||||
defaults apply".
|
||||
*/}}
|
||||
{{- define "convertx.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 "convertx.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.convertx.securityContext" can itself be erased, and indexing into nil
|
||||
inside a template is not an error but silently yields nothing.
|
||||
*/}}
|
||||
{{- define "convertx.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).
|
||||
|
||||
These defaults 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.
|
||||
|
||||
TWO OF THEM REPRODUCE A DELIBERATE DEVIATION AND MUST NOT BE "TIDIED":
|
||||
|
||||
- runAsUser / runAsGroup are 1000, not the repo's arbitrary 20000. The image
|
||||
creates its convertx user at uid 1000 and owns /home/convertx with it, and
|
||||
that uid owns the data on the PersistentVolume. Restoring the repo default
|
||||
here would move the runtime uid off the volume's data.
|
||||
- the probes point at /healthcheck, not at "/". "/" answers 302 in every
|
||||
default configuration of this chart, and the redirect target changes once
|
||||
the first account exists. The port name "http" is part of the context and
|
||||
belongs here too: a default that loses it leaves the workload with a probe
|
||||
that cannot succeed.
|
||||
*/}}
|
||||
{{- define "convertx.podSecurityContext" -}}
|
||||
{{- $given := (fromYaml (include "convertx.subBlock" (dict "block" . "key" "pod" "path" "convertx.securityContext"))).value -}}
|
||||
{{- $defaults := dict
|
||||
"runAsNonRoot" true
|
||||
"runAsUser" 1000
|
||||
"runAsGroup" 1000
|
||||
"fsGroup" 1000
|
||||
"fsGroupChangePolicy" "Always"
|
||||
"seccompProfile" (dict "type" "RuntimeDefault") -}}
|
||||
{{- include "convertx.defaultedDict" (dict "defaults" $defaults "given" $given "path" "convertx.securityContext.pod") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "convertx.containerSecurityContext" -}}
|
||||
{{- $given := (fromYaml (include "convertx.subBlock" (dict "block" . "key" "container" "path" "convertx.securityContext"))).value -}}
|
||||
{{- $defaults := dict
|
||||
"allowPrivilegeEscalation" false
|
||||
"readOnlyRootFilesystem" true
|
||||
"runAsNonRoot" true
|
||||
"capabilities" (dict "drop" (list "ALL") "add" (list)) -}}
|
||||
{{- include "convertx.defaultedDict" (dict "defaults" $defaults "given" $given "path" "convertx.securityContext.container") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "convertx.livenessProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (dict "path" "/healthcheck" "port" "http")
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 3 -}}
|
||||
{{- include "convertx.defaultedDict" (dict "defaults" $defaults "given" . "path" "convertx.livenessProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "convertx.readinessProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (dict "path" "/healthcheck" "port" "http")
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 3 -}}
|
||||
{{- include "convertx.defaultedDict" (dict "defaults" $defaults "given" . "path" "convertx.readinessProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "convertx.startupProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (dict "path" "/healthcheck" "port" "http")
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 30 -}}
|
||||
{{- include "convertx.defaultedDict" (dict "defaults" $defaults "given" . "path" "convertx.startupProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The effective resources block, fed into the resources helper above so that an
|
||||
erased "resources:" still yields the chart's requests and computed limits
|
||||
instead of a container without any resource accounting.
|
||||
|
||||
The numbers reproduce values.yaml, which carries the reasoning: they are sized
|
||||
for converter subprocesses (LibreOffice, Calibre, ImageMagick, ffmpeg), not
|
||||
for an idle web server, and the ephemeral-storage figure has to cover the two
|
||||
emptyDirs this chart mounts.
|
||||
*/}}
|
||||
{{- define "convertx.effectiveResources" -}}
|
||||
{{- $defaults := dict
|
||||
"limitFactor" 3
|
||||
"requests" (dict "cpu" 0.25 "memory" "1Gi" "ephemeralStorage" "1Gi") -}}
|
||||
{{- $merged := fromYaml (include "convertx.defaultedDict" (dict "defaults" $defaults "given" . "path" "convertx.resources")) -}}
|
||||
{{- include "convertx.resources" $merged -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render a values boolean as the literal string the application accepts.
|
||||
|
||||
Input: dict "value" <bool|string|nil> "path" <values path, for the error>
|
||||
"default" <bool, used when the value is nil>
|
||||
|
||||
Why this is not just "{{ .Values.x }}" (README, "Values that must not lie",
|
||||
point 6): ConvertX parses each of these variables as
|
||||
|
||||
process.env.X?.toLowerCase() === "true"
|
||||
|
||||
so ANYTHING that is not the literal "true" means false - "yes", "1", "on",
|
||||
"TRUE " with a trailing space. Two of the three variables this helper renders
|
||||
open the instance up (ACCOUNT_REGISTRATION, ALLOW_UNAUTHENTICATED), and a
|
||||
value written to switch one of them ON that silently lands on OFF is the kind
|
||||
of failure nobody goes looking for, because the symptom is the safe state.
|
||||
|
||||
So: a bool renders as "true"/"false", the strings "true"/"false" render
|
||||
normalized to lower case, nil takes the given default, and everything else
|
||||
ABORTS THE RENDER naming the values path. Go's "not" is deliberately unused
|
||||
here - it treats every non-empty string as true, so "not \"false\"" is false
|
||||
and the string form would flip the wrong way.
|
||||
*/}}
|
||||
{{- define "convertx.boolString" -}}
|
||||
{{- $v := .value -}}
|
||||
{{- if kindIs "invalid" $v -}}
|
||||
{{- $v = .default -}}
|
||||
{{- end -}}
|
||||
{{- if kindIs "bool" $v -}}
|
||||
{{- ternary "true" "false" $v -}}
|
||||
{{- else -}}
|
||||
{{- $s := lower (printf "%v" $v) -}}
|
||||
{{- if or (eq $s "true") (eq $s "false") -}}
|
||||
{{- $s -}}
|
||||
{{- else -}}
|
||||
{{- fail (printf "%s must be true or false, got %q - ConvertX compares this variable against the literal string \"true\", so any other spelling would silently mean false" .path $v) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render AUTO_DELETE_EVERY_N_HOURS.
|
||||
|
||||
An explicit 0 is the documented way to switch the cleanup off, so this cannot
|
||||
go through Helm's "default": that swallows a 0 and would restore 24-hour
|
||||
deletion on exactly the install that asked for none. Same nil-check shape as
|
||||
the replicas helper, and for the same reason.
|
||||
*/}}
|
||||
{{- define "convertx.autoDeleteEveryNHours" -}}
|
||||
{{- if kindIs "invalid" . -}}
|
||||
24
|
||||
{{- else -}}
|
||||
{{- include "convertx.formatNumber" . -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,34 @@
|
||||
{{- 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 }}
|
||||
# ConvertX enforces no upload limit of its own (it starts its HTTP server
|
||||
# with maxRequestBodySize: Number.MAX_SAFE_INTEGER), so these two
|
||||
# annotations are the ONLY ceiling on an upload. Both spellings are set
|
||||
# because the two nginx ingress controllers read different keys.
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: {{ .Values.ingress.maxUploadFileSize }}
|
||||
nginx.org/client-max-body-size: {{ .Values.ingress.maxUploadFileSize }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
rules:
|
||||
- host: {{ required "A Domain/Host must be provided" .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" .Values.ingress.domain }}
|
||||
secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
{{- end }}
|
||||
@@ -0,0 +1,13 @@
|
||||
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 }}
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
||||
spec:
|
||||
itemPath: {{ required "A Secret-Reference for the JWT signing key must be provided (secrets.jwtSecretRef)" .Values.secrets.jwtSecretRef }}
|
||||
@@ -0,0 +1,13 @@
|
||||
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
|
||||
@@ -0,0 +1,201 @@
|
||||
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 "convertx.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.convertx.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:
|
||||
# Hash of the rendered OnePasswordItem, so that repointing
|
||||
# secrets.jwtSecretRef at a DIFFERENT vault item really rolls the pod
|
||||
# instead of leaving the old signing key in the running environment.
|
||||
# A change of the item's CONTENT inside the vault is not covered -
|
||||
# Helm never sees it; see the comment on secrets in values.yaml.
|
||||
# The name differs from checksum/config on purpose: that one promises
|
||||
# "the config changed", this one only "the config source changed".
|
||||
#
|
||||
# operator.1password.io/auto-restart is deliberately NOT set: the
|
||||
# 1Password operator only restarts Deployments, so on a StatefulSet it
|
||||
# is a silent no-op that advertises a guarantee it does not give.
|
||||
checksum/secret-ref: {{ include (print $.Template.BasePath "/convertx.secret.yaml") . | sha256sum }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
{{- include "convertx.podSecurityContext" .Values.convertx.securityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
||||
image: "ghcr.io/c4illin/convertx:{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
||||
# THE IMAGE ENTRYPOINT IS BYPASSED ON PURPOSE. READ THIS BEFORE THE
|
||||
# NEXT APP-VERSION BUMP - IT IS THE ONE BET THIS CHART MAKES.
|
||||
#
|
||||
# ghcr.io/c4illin/convertx declares
|
||||
# ENTRYPOINT ["/entrypoint.sh"]
|
||||
# CMD ["bun", "run", "dist/src/index.js"]
|
||||
# and carries no USER, so the image runs as root by default and
|
||||
# /entrypoint.sh is what drops it to uid 1000 with gosu. Setting
|
||||
# "command" here replaces the ENTRYPOINT (and discards the CMD), so
|
||||
# the application is started directly and Kubernetes does the
|
||||
# privilege work instead: runAsUser/runAsGroup 1000 in the pod
|
||||
# security context, and fsGroup 1000 in place of the entrypoint's
|
||||
# recursive chown of /app/data. The hardening baseline of this repo
|
||||
# stays intact - no root, no capabilities, read-only root fs.
|
||||
#
|
||||
# What the entrypoint of v0.19.0 actually does, read rather than
|
||||
# assumed: nothing but uid/gid plumbing. No migration, no config
|
||||
# generation, no secret generation. ConvertX creates and migrates
|
||||
# its SQLite schema in-process at import time, so starting it
|
||||
# directly is migration-safe. Its very first branch is
|
||||
# if [ -z "$PUID" ] || [ "$PUID" -eq 0 ]; then ... exec "$@"
|
||||
# i.e. with PUID unset it is already a pure pass-through - which is
|
||||
# why bypassing it costs nothing TODAY and is exactly why the risk
|
||||
# below is about tomorrow. The one line we do lose is
|
||||
# "export HOME=/home/convertx", and this chart sets HOME itself
|
||||
# below rather than relying on the runtime's passwd lookup.
|
||||
#
|
||||
# THE RISK: if upstream ever moves real work into the entrypoint - a
|
||||
# data migration is the obvious candidate - this chart skips it
|
||||
# silently. There is no error, no log line and no failed probe; the
|
||||
# application simply starts against an unmigrated volume. So
|
||||
# RE-READ entrypoint.sh AT EVERY APP-VERSION UPDATE, not only on
|
||||
# major jumps, and if it has grown anything beyond uid/gid handling,
|
||||
# stop and reconsider this override.
|
||||
command:
|
||||
- bun
|
||||
- run
|
||||
- dist/src/index.js
|
||||
|
||||
# Not decoration. The application resolves its data paths relative
|
||||
# to the working directory - uploadsDir is "./data/uploads/" and
|
||||
# outputDir is "./data/output/", hardcoded and configurable by no
|
||||
# environment variable. The image sets WORKDIR /app, so this only
|
||||
# restates it; it is here because a different working directory
|
||||
# would put every upload somewhere other than the mounted PVC, and
|
||||
# the application would report no error at all.
|
||||
workingDir: /app
|
||||
|
||||
env:
|
||||
# The entrypoint would have exported this; the image declares no
|
||||
# ENV HOME, so without this line HOME comes from the container
|
||||
# runtime's /etc/passwd lookup for uid 1000. That happens to
|
||||
# resolve to /home/convertx in this image - and "happens to" is
|
||||
# not a basis for the directory LibreOffice, Calibre, Inkscape and
|
||||
# fontconfig write their profiles into on the first conversion.
|
||||
# Set explicitly, and mounted writable below.
|
||||
- name: HOME
|
||||
value: "/home/convertx"
|
||||
# The port the application binds. containerPort, the Service's
|
||||
# targetPort and the three probes all name the same 3000; moving
|
||||
# it means moving all four, which is why it is pinned here rather
|
||||
# than exposed as a value.
|
||||
- name: PORT
|
||||
value: "3000"
|
||||
# Signs the session cookies. Without it ConvertX falls back to
|
||||
# randomUUID() per process, so every restart logs everyone out -
|
||||
# see secrets.jwtSecretRef in values.yaml.
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
||||
key: jwt-secret
|
||||
# The three booleans go through convertx.boolString, which renders
|
||||
# the literal "true"/"false" the application's strict comparison
|
||||
# needs and aborts the render on anything else.
|
||||
- name: ACCOUNT_REGISTRATION
|
||||
value: "{{ include "convertx.boolString" (dict "value" .Values.convertx.accountRegistration "default" false "path" "convertx.accountRegistration") }}"
|
||||
- name: ALLOW_UNAUTHENTICATED
|
||||
value: "{{ include "convertx.boolString" (dict "value" .Values.convertx.allowUnauthenticated "default" false "path" "convertx.allowUnauthenticated") }}"
|
||||
- name: HTTP_ALLOWED
|
||||
value: "{{ include "convertx.boolString" (dict "value" .Values.convertx.httpAllowed "default" false "path" "convertx.httpAllowed") }}"
|
||||
- name: AUTO_DELETE_EVERY_N_HOURS
|
||||
value: "{{ include "convertx.autoDeleteEveryNHours" .Values.convertx.autoDeleteEveryNHours }}"
|
||||
{{- range $value := .Values.convertx.env }}
|
||||
{{- if $value.value }}
|
||||
- name: {{ $value.name | quote }}
|
||||
# quote is required, not cosmetic: a value like "8080" or "true"
|
||||
# renders unquoted as a YAML number/bool, and the API server then
|
||||
# rejects the whole manifest.
|
||||
value: {{ tpl $value.value $ | quote }}
|
||||
{{- else if $value.valueFrom }}
|
||||
{{- if $value.valueFrom.secretKeyRef }}
|
||||
- name: {{ $value.name }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ tpl $value.valueFrom.secretKeyRef.name $ | quote }}
|
||||
key: {{ tpl $value.valueFrom.secretKeyRef.key $ | quote }}
|
||||
{{- else if $value.valueFrom.configMapKeyRef }}
|
||||
- name: {{ $value.name }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ tpl $value.valueFrom.configMapKeyRef.name $ | quote }}
|
||||
key: {{ tpl $value.valueFrom.configMapKeyRef.key $ | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
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 "convertx.livenessProbe" .Values.convertx.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- include "convertx.readinessProbe" .Values.convertx.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- include "convertx.startupProbe" .Values.convertx.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "convertx.effectiveResources" .Values.convertx.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- include "convertx.containerSecurityContext" .Values.convertx.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
# SQLite database, uploads and converted output - everything the
|
||||
# application persists lives under this one path.
|
||||
- mountPath: /app/data
|
||||
name: data
|
||||
# Two writable emptyDirs so the root filesystem can stay
|
||||
# read-only. Neither is a precaution: ConvertX itself writes only
|
||||
# under /app/data, but it runs its conversions as subprocesses
|
||||
# (soffice, ebook-convert, magick, gs, xelatex, ffmpeg...) that
|
||||
# inherit its environment, and those need scratch space.
|
||||
#
|
||||
# /home/convertx is HOME. LibreOffice creates its user profile
|
||||
# there on first use - it is invoked without
|
||||
# -env:UserInstallation, so there is no way to point it elsewhere
|
||||
# - and Calibre, Inkscape, ImageMagick and fontconfig do the same
|
||||
# with their own config and cache directories. The directory
|
||||
# exists in the image (useradd -m) and is owned by 1000:1000, but
|
||||
# it sits on the read-only root filesystem, so it has to be
|
||||
# overmounted to be usable.
|
||||
- mountPath: /home/convertx
|
||||
name: home
|
||||
# /tmp is the scratch directory of the toolchain: Ghostscript,
|
||||
# TeX Live and Calibre all use it during a conversion.
|
||||
#
|
||||
# Both of these count against the container's ephemeral-storage
|
||||
# limit - see the resources comment in values.yaml.
|
||||
- mountPath: /tmp
|
||||
name: tmp
|
||||
restartPolicy: Always
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Release.Name }}-{{ .Chart.Name }}-pvc
|
||||
- name: home
|
||||
emptyDir: {}
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
@@ -0,0 +1,212 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||
"title": "convertx 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) and the free-form Ingress annotation map - 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).",
|
||||
"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 convertx.replicas. PVC, Secret, Service and Ingress stay in place.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"secrets": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"jwtSecretRef": {
|
||||
"description": "REQUIRED. op:// path of the 1Password item carrying the field 'jwt-secret', which becomes JWT_SECRET. Enforced by required() in the template rather than by a 'required' array here: the key ships as null, so JSON Schema's required - which tests presence, not null-ness - could never fire on it.",
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"convertx": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"replicas": {
|
||||
"description": "0 is allowed and scales the workload down. Values above 1 are accepted by the schema but do not work: the SQLite database and the uploads share one ReadWriteOnce PVC.",
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"accountRegistration": {
|
||||
"description": "ACCOUNT_REGISTRATION - lets anyone with the URL create an account. Defaults to false; upstream warns against enabling it on an internet-facing instance. A string is accepted for the 'true'/'false' spellings only; anything else aborts the render with a message naming this path, because the application would silently read it as false.",
|
||||
"type": ["boolean", "string", "null"]
|
||||
},
|
||||
"allowUnauthenticated": {
|
||||
"description": "ALLOW_UNAUTHENTICATED - drops authentication entirely. Defaults to false; upstream describes this as local-only. Same accepted spellings as accountRegistration.",
|
||||
"type": ["boolean", "string", "null"]
|
||||
},
|
||||
"httpAllowed": {
|
||||
"description": "HTTP_ALLOWED - clears the 'secure' flag on the session cookies. Only needed when ConvertX is served without TLS in front; behind this chart's Ingress the browser speaks https, so the default false is correct. Same accepted spellings as accountRegistration.",
|
||||
"type": ["boolean", "string", "null"]
|
||||
},
|
||||
"autoDeleteEveryNHours": {
|
||||
"description": "AUTO_DELETE_EVERY_N_HOURS - how old a job may get before ConvertX deletes its uploads and output. 0 disables the cleanup and is honoured as such (the chart uses a nil check, not Helm's 'default', which would swallow the 0). This is the retention policy for the PVC.",
|
||||
"type": ["integer", "number", "null"],
|
||||
"minimum": 0
|
||||
},
|
||||
"securityContext": {
|
||||
"$ref": "#/definitions/securityContext"
|
||||
},
|
||||
"livenessProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"readinessProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"startupProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"env": {
|
||||
"$ref": "#/definitions/env"
|
||||
},
|
||||
"resources": {
|
||||
"$ref": "#/definitions/resources"
|
||||
}
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"description": "The data PVC: SQLite database, uploads and converted output. Not null-safe on purpose - an erased block yields a PVC without a size, which the API server rejects at apply time rather than the chart sizing the volume silently.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"storageClass": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ingress": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Gates the whole Ingress template. false frees BOTH clusterIssuer and domain: nothing outside the Ingress reads the domain in this chart, because ConvertX has no base-URL or redirect-URI variable.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"maxUploadFileSize": {
|
||||
"description": "Rendered into the ingress-nginx proxy-body-size annotation, so it only takes effect while the Ingress is enabled. It is the only upload ceiling that exists - ConvertX enforces none of its own.",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"clusterIssuer": {
|
||||
"description": "REQUIRED while ingress.enabled is true (enforced by required() in the template).",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"domain": {
|
||||
"description": "REQUIRED while ingress.enabled is true (enforced by required() in the template).",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"className": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"annotations": {
|
||||
"description": "Extra Ingress annotations, merged with the cert-manager and proxy-body-size annotations the chart always sets. Free-form keys by nature.",
|
||||
"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": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityContext": {
|
||||
"description": "May be null / empty at any level: the chart then falls back to its hardening defaults (issue #99) instead of rendering an unhardened workload. Those defaults pin runAsUser/runAsGroup 1000 - the uid the image creates and the uid that owns the data on the PersistentVolume - rather than the repo's arbitrary 20000.",
|
||||
"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"]
|
||||
},
|
||||
"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 defaults target /healthcheck on the port named http - overriding the path to '/' would probe a 302 whose target changes once the first account exists.",
|
||||
"type": ["object", "null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
# 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. PVC, Secret, Service
|
||||
# and Ingress stay in place; scaleDown: false brings the workload back up.
|
||||
scaleDown: false
|
||||
|
||||
secrets:
|
||||
# op:// item path for the JWT signing key. REQUIRED - the chart aborts the
|
||||
# render without it, and that is deliberate rather than convenient.
|
||||
#
|
||||
# ConvertX signs its session cookies with JWT_SECRET and falls back to
|
||||
# randomUUID() when the variable is unset (src/services/user.ts:
|
||||
# "secret: process.env.JWT_SECRET ?? randomUUID()"). That fallback is
|
||||
# per PROCESS, not per install: every restart - a node drain, an image
|
||||
# update, an OOM kill - mints a fresh secret and silently invalidates every
|
||||
# session. Nothing logs it; users simply find themselves logged out. A value
|
||||
# that has to survive a restart is a secret, so it is one here.
|
||||
#
|
||||
# The item must carry this field, spelled exactly like this, because the
|
||||
# 1Password operator copies an item's FIELD NAMES into the Secret's keys:
|
||||
#
|
||||
# jwt-secret a long random string (openssl rand -hex 32)
|
||||
#
|
||||
# A field spelled differently does not fail the install: the operator writes
|
||||
# a Secret without that key and the pod stays in CreateContainerConfigError,
|
||||
# which looks like a broken chart rather than a typo in a vault item.
|
||||
jwtSecretRef: null
|
||||
|
||||
convertx:
|
||||
# Replica count for this workload; 0 is allowed and scales it down.
|
||||
# The chart-global scaleDown flag takes precedence.
|
||||
#
|
||||
# Raising this above 1 does not work and the chart does not stop you, so the
|
||||
# reason is here: the SQLite database, the uploads and the converted output
|
||||
# all live on the single ReadWriteOnce PVC below, which is a plain chart
|
||||
# object rather than a volumeClaimTemplate. A second pod normally never
|
||||
# schedules ("Multi-Attach error for volume"); on a genuinely RWX storage
|
||||
# class it would schedule and then corrupt the WAL-mode SQLite file that two
|
||||
# processes are now writing. On top of that each pod would sign cookies with
|
||||
# its own JWT_SECRET only if the secret were unset - which is exactly why
|
||||
# secrets.jwtSecretRef above is required.
|
||||
replicas: 1
|
||||
|
||||
# --- Two switches that open the instance up. Both default to false. ---
|
||||
#
|
||||
# UPSTREAM WARNS EXPLICITLY AGAINST ENABLING EITHER ON AN INTERNET-FACING
|
||||
# INSTANCE, and this chart puts an Ingress with a public DNS name in front
|
||||
# by default, so that warning applies to the standard install here.
|
||||
#
|
||||
# Both are parsed strictly by the application: it compares the lowercased
|
||||
# string against "true" and treats everything else as false. The chart
|
||||
# therefore accepts only a boolean or the strings "true"/"false" (any case)
|
||||
# and ABORTS THE RENDER on anything else - "yes" or "1" would otherwise be
|
||||
# filed under "off" without a word, which is the wrong direction to fail in
|
||||
# for a value whose whole job is to open the service up.
|
||||
|
||||
# Lets anyone with the URL create an account. Note what it does NOT gate:
|
||||
# the FIRST account is always creatable, because a ConvertX with no users
|
||||
# serves a setup form to whoever opens the page first. Keeping this false
|
||||
# protects accounts two and onwards, not account one.
|
||||
accountRegistration: false
|
||||
|
||||
# Drops authentication entirely - anyone reaching the Service can upload
|
||||
# files and read every conversion. Upstream describes this as local-only.
|
||||
allowUnauthenticated: false
|
||||
|
||||
# Clears the "secure" flag on the session cookies (the app computes
|
||||
# "secure: !HTTP_ALLOWED"). Only needed when ConvertX is served over plain
|
||||
# http, and that is NOT our case: the Ingress terminates TLS, so the browser
|
||||
# speaks https and sets the cookie happily while only the in-cluster hop
|
||||
# between ingress controller and pod is http - which the browser never sees.
|
||||
# Turning this on behind our Ingress weakens the cookies for no gain.
|
||||
# (Same shape as outline's forceHttps, and the same reason to leave it off.)
|
||||
httpAllowed: false
|
||||
|
||||
# How often ConvertX deletes jobs - uploads and converted output - older
|
||||
# than this many hours. 0 disables the cleanup entirely.
|
||||
#
|
||||
# This is the retention policy for the PVC below, which is why it is a value
|
||||
# and not left at the upstream default. With cleanup disabled the volume
|
||||
# grows with every conversion and nothing reclaims it; size storage.amount
|
||||
# for that case, or accept that the pod eventually fills its volume.
|
||||
#
|
||||
# An explicit 0 is honoured (the chart uses a nil check, not Helm's
|
||||
# "default", which would swallow the 0 and silently restore 24-hour
|
||||
# deletion on a value set precisely to switch it off).
|
||||
autoDeleteEveryNHours: 24
|
||||
|
||||
# DOCUMENTED DEVIATION FROM THE HARDENING BASELINE - runAsUser/runAsGroup.
|
||||
#
|
||||
# The deviation is about the NUMBER only: nothing in the baseline is
|
||||
# relaxed. The repo default is an arbitrary uid (20000); this chart pins
|
||||
# 1000, and the reason is the image, not a preference.
|
||||
#
|
||||
# ghcr.io/c4illin/convertx carries NO USER instruction - verified in the
|
||||
# published v0.19.0 image config, where "User" is empty - so without a
|
||||
# securityContext the pod would run as root. Its Dockerfile creates
|
||||
# "convertx" with uid 1000, gid 1000 and home /home/convertx (useradd -u
|
||||
# 1000 -g convertx -d /home/convertx -m), and the image's entrypoint exists
|
||||
# only to drop to that user with gosu.
|
||||
#
|
||||
# 1000 rather than 20000 for two reasons that both have to hold:
|
||||
# - /home/convertx belongs to 1000:1000 in the image, and HOME is where
|
||||
# LibreOffice, Calibre and fontconfig put their profiles. An arbitrary
|
||||
# uid gets a HOME of "/" from the runtime's passwd lookup instead.
|
||||
# - the uid owns data on a PersistentVolume, so changing it later makes
|
||||
# existing conversions inaccessible. This number is not free to tidy.
|
||||
securityContext:
|
||||
pod:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
# fsGroup is what replaces the entrypoint's recursive chown of
|
||||
# /app/data: the kubelet applies the group to the mounted volume, so
|
||||
# the container can write it as uid 1000 without any privileged step.
|
||||
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
|
||||
# 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 securityContext.container, which wins over this value.
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
container:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add: []
|
||||
|
||||
# ConvertX serves GET /healthcheck: a static 200 with {"status":"ok"},
|
||||
# explicitly exempted from authentication (src/pages/healthcheck.tsx,
|
||||
# "auth: false"). All three probes use it.
|
||||
#
|
||||
# Why NOT "/", the obvious default: with the chart defaults "/" is never a
|
||||
# 200. It answers 302 to /setup while no user exists and 302 to /login
|
||||
# afterwards, so a probe against it would pass on a redirect whose target
|
||||
# changes the moment somebody registers - a green signal that means
|
||||
# something different on day two than on day one. httpGet does count 3xx as
|
||||
# success, which is what makes that the quiet kind of wrong.
|
||||
#
|
||||
# What the endpoint proves, and what it does not: the handler touches
|
||||
# neither the database nor the volume, so it cannot report a broken PVC.
|
||||
# It is still the right readiness target here, because the SQLite database
|
||||
# is opened at module import - before app.listen() is reached - so a process
|
||||
# that answers /healthcheck at all has already opened its database. There is
|
||||
# no external dependency in this chart to hang readiness on; the "database"
|
||||
# is a file on the pod's own volume.
|
||||
#
|
||||
# WEBROOT SHIFTS THIS PATH. The application mounts every route under the
|
||||
# WEBROOT prefix, /healthcheck included. This chart does not offer WEBROOT
|
||||
# as a value (see the env block below), and setting it through env without
|
||||
# also moving these three paths leaves the pod probing a 404 forever.
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthcheck
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthcheck
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
# The startup probe gates liveness/readiness, so those need no
|
||||
# initialDelaySeconds. Budget = periodSeconds * failureThreshold = 300s.
|
||||
#
|
||||
# Generous on purpose. At import time - under NODE_ENV=production, which
|
||||
# the image sets - ConvertX shells out to roughly twenty converter binaries
|
||||
# to log their versions (soffice, ebook-convert, xelatex, magick, ffmpeg,
|
||||
# markitdown and the rest). Those are fire-and-forget and do not block
|
||||
# app.listen(), but they all contend for the pod's CPU at exactly the moment
|
||||
# the first probe arrives, and the first soffice/ebook-convert run also
|
||||
# builds its profile under HOME. A tight budget turns a slow node into a
|
||||
# crash loop.
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /healthcheck
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 30
|
||||
|
||||
# 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 where the upstream variables this chart does not expose as values
|
||||
# belong - BRANDING, LANGUAGE, HIDE_HISTORY, MAX_CONVERT_PROCESS,
|
||||
# UNAUTHENTICATED_USER_SHARING, TZ, FFMPEG_ARGS, FFMPEG_OUTPUT_ARGS. They
|
||||
# change the UI or a converter's behaviour and nothing this chart renders,
|
||||
# so they need no value of their own.
|
||||
#
|
||||
# THREE VARIABLES DO NOT BELONG HERE, and they fail in three different ways:
|
||||
#
|
||||
# WEBROOT moves EVERY route, /healthcheck included, so the three
|
||||
# probes above start failing against a 404 while the app is
|
||||
# perfectly healthy - and this chart's Ingress still maps "/".
|
||||
# Serving under a sub-path needs the probe paths and the
|
||||
# Ingress path changed together; the chart does not do that
|
||||
# for you.
|
||||
# PUID/PGID are read by the image entrypoint, which this chart bypasses
|
||||
# /UMASK (see the command: override in the StatefulSet). They reach
|
||||
# nothing and configure nothing here. Set the uid through
|
||||
# securityContext.pod above, which is what actually applies.
|
||||
# PORT the app would listen on it, but containerPort, the Service
|
||||
# and the probes all still name 3000. Moving the port means
|
||||
# moving all four, so the chart pins it instead.
|
||||
#
|
||||
# Additive means additive: an entry repeating a name the chart already sets
|
||||
# does not replace it. At install the later entry - yours - wins; on an
|
||||
# EXISTING release Kubernetes merges env by name, the pair collapses to the
|
||||
# chart's own value, and helm upgrade still reports success. Read the value
|
||||
# back off the StatefulSet rather than trusting a green upgrade.
|
||||
env: []
|
||||
|
||||
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 numbers are sized for conversions, not for an idle web server, and
|
||||
# each one has a reason:
|
||||
#
|
||||
# cpu 0.25 An idle Bun process uses almost nothing, but a
|
||||
# conversion saturates whatever it can get and the startup
|
||||
# fires ~20 version probes at once. Per repo convention no
|
||||
# CPU LIMIT is computed, so this is the scheduling floor
|
||||
# and the throttling share under contention, not a cap -
|
||||
# a conversion may burst well past it on an idle node.
|
||||
#
|
||||
# memory 1Gi soffice --headless is a few hundred MB on its own,
|
||||
# ebook-convert pulls in Qt WebEngine, and ImageMagick on
|
||||
# a large raster can want more than either. 1Gi requested,
|
||||
# 3Gi limit (limitFactor 3). Memory is NOT compressible:
|
||||
# a conversion that crosses the limit is an OOM kill of
|
||||
# the whole pod, so raise the request before feeding the
|
||||
# instance large video or raw-image work.
|
||||
#
|
||||
# ephemeral- 1Gi, and this one is easy to get wrong: EMPTYDIR VOLUMES
|
||||
# storage COUNT AGAINST IT. The chart mounts two (/tmp and
|
||||
# /home/convertx) for the converter toolchain, and
|
||||
# Ghostscript, TeX and Calibre all scratch to /tmp. The
|
||||
# uploads and the converted output do NOT count - those
|
||||
# live on the PVC - so the figure covers scratch space
|
||||
# only. Crossing the 3Gi limit evicts the pod.
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 0.25
|
||||
memory: 1Gi
|
||||
ephemeralStorage: 1Gi
|
||||
|
||||
# The volume behind /app/data: the SQLite database, every uploaded file and
|
||||
# every converted result. There is no second store and no object storage.
|
||||
#
|
||||
# Not null-safe on purpose, following the repo's documented exception: an
|
||||
# erased block yields a PVC without a size, which the API server rejects at
|
||||
# apply time. That is louder than a volume quietly sized by the chart.
|
||||
storage:
|
||||
amount: 10Gi
|
||||
storageClass: null
|
||||
|
||||
ingress:
|
||||
# Gates the whole Ingress template. Checked in both directions as the style
|
||||
# guide requires: NOTHING outside the Ingress reads ingress.domain in this
|
||||
# chart - ConvertX has no base-URL, public-URL or redirect-URI variable at
|
||||
# all (verified against the complete env list in src/helpers/env.ts) - so
|
||||
# switching the Ingress off frees BOTH domain and clusterIssuer here.
|
||||
enabled: true
|
||||
|
||||
# THE ONLY UPLOAD CEILING THERE IS.
|
||||
#
|
||||
# ConvertX sets no size limit of its own: it starts Elysia with
|
||||
# maxRequestBodySize: Number.MAX_SAFE_INTEGER, and v0.19.0 has no upload
|
||||
# variable of any kind. So unlike outline - where an app limit and an
|
||||
# ingress limit sat a factor of ten apart and the smaller one won
|
||||
# confusingly - there is exactly one number here, and it is this one.
|
||||
#
|
||||
# Two consequences worth knowing before changing it:
|
||||
# - A file above this limit never reaches ConvertX. The user gets the
|
||||
# ingress controller's 413 page, not a message from the application,
|
||||
# and nothing appears in the pod log.
|
||||
# - With ingress.enabled: false there is NO ceiling from this chart at
|
||||
# all, and whatever proxy you put in front brings its own - usually
|
||||
# ingress-nginx's 1m default, which is small enough to look like a
|
||||
# broken upload button.
|
||||
#
|
||||
# Defaulted generously because the point of the application is converting
|
||||
# video and large documents. What it costs: the body transits the ingress
|
||||
# controller and then lands on the PVC, so raising it means checking
|
||||
# storage.amount as well.
|
||||
maxUploadFileSize: 1024m
|
||||
|
||||
clusterIssuer: null
|
||||
domain: null
|
||||
className: nginx
|
||||
# Extra annotations for the Ingress; merged with the cert-manager
|
||||
# cluster-issuer and proxy-body-size annotations the chart always sets.
|
||||
annotations: {}
|
||||
+2
-2
@@ -5,8 +5,8 @@ icon: https://raw.githubusercontent.com/lukevella/rallly/main/apps/landing/publi
|
||||
|
||||
type: application
|
||||
|
||||
version: 8.1.7+up4.15.3
|
||||
appVersion: "4.15.3"
|
||||
version: 8.1.6+up4.15.2
|
||||
appVersion: "4.15.2"
|
||||
|
||||
maintainers:
|
||||
- name: jklein
|
||||
@@ -334,6 +334,29 @@
|
||||
datasourceTemplate: "docker",
|
||||
depTypeTemplate: "app-version",
|
||||
},
|
||||
{
|
||||
description: [
|
||||
"convertx: the registry carries versioned tags (v0.19.0 and its predecessors) next",
|
||||
"to THREE moving ones - 'latest', 'main' and a 'pr-<n>' per open pull request.",
|
||||
"v0.19.0 and 'latest' currently share the same digest (sha256:8590a21a…), which is",
|
||||
"exactly the trap: an appVersion of 'latest' would look correct today and change the",
|
||||
"running image on the next upstream push, without a commit here. 'main' and 'pr-<n>'",
|
||||
"are worse still - they carry unreviewed code.",
|
||||
"The anchored regex versioning below is what keeps all three out: it demands the v",
|
||||
"prefix and three numeric parts, which none of them can satisfy. Precedent and the",
|
||||
"full argument: the ytdlp-web-player block near the end of this file.",
|
||||
],
|
||||
customType: "regex",
|
||||
managerFilePatterns: ["convertx/Chart.yaml"],
|
||||
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
|
||||
depNameTemplate: "ghcr.io/c4illin/convertx",
|
||||
datasourceTemplate: "docker",
|
||||
depTypeTemplate: "app-version",
|
||||
// The "v" stays outside every capture group: it is part of the tag and
|
||||
// carries no ordering. A group is a claim about how the value is
|
||||
// compared, and the anchors are what exclude latest/main/pr-<n>.
|
||||
versioningTemplate: "regex:^v(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)$",
|
||||
},
|
||||
{
|
||||
customType: "regex",
|
||||
managerFilePatterns: ["cyberchef/Chart.yaml"],
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# Project-specific half of the SonarQube analysis. The workflow is the shared
|
||||
# block in workflow-util/pipelines and is identical for every repository that
|
||||
# uses it; everything that is about THIS repository lives here.
|
||||
#
|
||||
# The key is the one the project already has on the server, given by the
|
||||
# maintainer. It is not derivable and must not be guessed: the project is
|
||||
# created in SonarQube first, and the key is then read off it.
|
||||
#
|
||||
# The first attempt here was a guess, "jlab-cluster_helm-charts", reasoned from
|
||||
# the instance serving two owners. It failed with
|
||||
#
|
||||
# ERROR You're not authorized to analyze this project or the project doesn't
|
||||
# exist on SonarQube and you're not authorized to create it.
|
||||
#
|
||||
# which is the message for BOTH a wrong key and a token without "Execute
|
||||
# Analysis" on the project - the server does not distinguish them, so a wrong
|
||||
# key here looks exactly like a permission problem over there. Once the key is
|
||||
# known to be right, that message means the token.
|
||||
sonar.projectKey=JLab-Helm-Charts
|
||||
|
||||
# NO sonar.projectName, deliberately.
|
||||
#
|
||||
# The project exists on the server, so the display name belongs to the server:
|
||||
# whoever renames it there expects the rename to hold. A name sent with the
|
||||
# analysis would overwrite it on every single run, silently, and the person who
|
||||
# renamed it would only see it snap back. A value that rewrites someone else's
|
||||
# setting without saying so is not a value this repository carries. If the name
|
||||
# is wrong, it gets fixed in SonarQube, not here.
|
||||
|
||||
# The whole tree, which is also the scanner's default - stated rather than
|
||||
# implied, because the absence of exclusions below is a decision and an empty
|
||||
# file would not show that.
|
||||
sonar.sources=.
|
||||
|
||||
# NO EXCLUSIONS, deliberately, for the first round.
|
||||
#
|
||||
# The interesting case is the Helm templates: because of {{ }} they are not
|
||||
# valid YAML, and what the Helm and YAML analysers do with them is something we
|
||||
# can only find out by letting them run over them once. Excluding them up front
|
||||
# would throw away the only chance to learn it, and it would exclude the 26
|
||||
# chart template directories that are the bulk of this repository.
|
||||
#
|
||||
# The same goes for the secret detection, which is the most valuable analyser
|
||||
# here: the premise of this repository is that no credential ever lands in it -
|
||||
# every secret is a 1Password OnePasswordItem reference - and until now nothing
|
||||
# checked that premise.
|
||||
#
|
||||
# If the first run produces noise that is genuinely not worth reading, the
|
||||
# exclusions belong here, with the finding they answer named in a comment. Not
|
||||
# before: an exclusion written on a guess hides whatever else matched it.
|
||||
@@ -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.2+up6.14.0
|
||||
appVersion: 6.14.0
|
||||
version: 10.0.1+up6.13.1
|
||||
appVersion: 6.13.1
|
||||
Reference in new issue
Block a user