Merge pull request #219 from jk-cluster/feat/ytdlp-web-player-chart
ytdlp-web-player: neuer Chart mit optionalem oauth2-proxy als Unterchart (1.0.0+upv1.2.0)
This commit is contained in:
23 files changed
+1706
-8
No files matched your search
@@ -37,6 +37,7 @@ on:
|
||||
- rss-bridge
|
||||
- searxng
|
||||
- bentopdf
|
||||
- ytdlp-web-player
|
||||
# freesailarr is deliberately absent: none of its nine images takes
|
||||
# its tag from appVersion (they are all pinned in its values.yaml),
|
||||
# so this workflow would rewrite appVersion and republish the chart
|
||||
|
||||
@@ -43,6 +43,17 @@ jobs:
|
||||
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 }}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
name: Publish YTDLP Web Player Helm Chart
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ytdlp-web-player/**
|
||||
jobs:
|
||||
publish-ytdlp-web-player:
|
||||
name: Publish YTDLP Web Player Helm Chart
|
||||
uses: ./.github/workflows/publish-helm-chart.yml
|
||||
with:
|
||||
chart-name: 'ytdlp-web-player'
|
||||
secrets: inherit
|
||||
@@ -70,6 +70,15 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: "Install Helm"
|
||||
uses: azure/setup-helm@v5
|
||||
# Both steps below refuse to run while a chart declares a dependency that
|
||||
# is not in its charts/ directory - lint with "[WARNING] chart directory
|
||||
# is missing these dependencies", package with "found in Chart.yaml, but
|
||||
# missing in charts/ directory", both exiting 1. Charts without a
|
||||
# dependencies: block are skipped without a word, so this costs nothing
|
||||
# for the rest of the repo. See ci/helm-dependencies.sh for why it is
|
||||
# "update" and not "build".
|
||||
- name: "Fetch chart dependencies"
|
||||
run: ci/helm-dependencies.sh "${{ matrix.chart }}"
|
||||
- name: "Lint Helm Chart"
|
||||
run: helm lint --strict "${{ matrix.chart }}"
|
||||
- name: "Package Helm Chart"
|
||||
|
||||
+23
@@ -1,6 +1,29 @@
|
||||
### Example user template template
|
||||
### Example user template
|
||||
|
||||
# Fetched Helm subchart archives and the lock file that records them (#214).
|
||||
#
|
||||
# Neither is committed, on purpose. The .tgz is a third-party binary that would
|
||||
# land in every diff of a dependency update and be reviewed by nobody; CI
|
||||
# fetches it with ci/helm-dependencies.sh before every lint, package, render
|
||||
# and install.
|
||||
#
|
||||
# The lock is left out for a concrete reason rather than by omission: Renovate
|
||||
# rewrites the version in Chart.yaml and cannot regenerate Chart.lock, so a
|
||||
# committed lock would be stale on arrival - and `helm dependency build`, the
|
||||
# only command that would benefit from it, fails outright when lock and
|
||||
# Chart.yaml disagree. Every dependency PR would come in red. `helm dependency
|
||||
# update` resolves from Chart.yaml instead and writes the lock itself.
|
||||
#
|
||||
# What is given up with it: the digest pinning a committed lock would provide.
|
||||
# The version range in Chart.yaml is an exact version, so what moves is only
|
||||
# what the upstream repo serves under that version.
|
||||
*/charts/*.tgz
|
||||
*/Chart.lock
|
||||
|
||||
# Local helm package output.
|
||||
*.tgz
|
||||
|
||||
# IntelliJ project files
|
||||
.idea
|
||||
*.iml
|
||||
|
||||
@@ -68,4 +68,5 @@ The binding style guide for all current and future charts lives in **README.md (
|
||||
- Probes: liveness/readiness/startup on every workload (exception: paperless consume CronJob), values-configurable with calibrated defaults.
|
||||
- Resources: requests as values defaults; limits auto-computed by the `_helpers.tpl` helper (`limitFactor` default 3; no CPU limits unless set explicitly).
|
||||
- Replicas per workload (explicit `0` allowed) plus a chart-global `scaleDown` flag that overrides them.
|
||||
- `icon:` mandatory in every `Chart.yaml`; `helm lint --strict` without findings; CI fixtures under `ci/<chart>/` or a justified exclusion; no chart dependencies (`charts/` directories are empty).
|
||||
- `icon:` mandatory in every `Chart.yaml`; `helm lint --strict` without findings; CI fixtures under `ci/<chart>/` or a justified exclusion.
|
||||
- **Chart dependencies are admissible but expensive** (the former "no dependencies" rule fell with #214; `ytdlp-web-player` is the only chart using one). A dependency has to be optional and off by default via `condition:`, and upstream's chart is consumed as it comes — where its defaults fall short of these conventions, that is **documented in our `values.yaml`, never patched**. The price to know before adding the next one: a subchart is invisible to everything downstream that watches Helm release versions, so a new upstream version — a security fix included — reaches a cluster only when the outer chart is republished (Renovate's built-in `helmv3` manager plus `ci/renovate-bump-chart.sh` is the whole safety net, and it belongs in a comment at the pinned version); and every helm command exits 1 while `charts/` is unfilled, which is why `ci/helm-dependencies.sh` runs before lint, package, render and install — including the render test that covers **all** charts on **every** PR. Full rules and the rest of the cost: README, "Chart dependencies".
|
||||
@@ -28,6 +28,7 @@ Collection of some self-made helm-charts. Each top-level directory is a standalo
|
||||
| `template-chart` | — | Scaffold for new charts (placeholder `xxx`), never published |
|
||||
| `urlaubsverwaltung` | Urlaubsverwaltung | Deployment, DB + OIDC secrets |
|
||||
| `voucher-vault` | VoucherVault | StatefulSet, DB + OIDC secrets |
|
||||
| `ytdlp-web-player` | ytdlp_web_player | Stateless Deployment, video cache and tool cache as emptyDirs (no PVC). **The only chart with a dependency**: an optional `oauth2-proxy` subchart, off by default, wired in front of the app because the application ships no authentication at all — see "Chart dependencies" below |
|
||||
| `zipline` | Zipline | StatefulSet, DB secret |
|
||||
|
||||
## Versioning
|
||||
@@ -42,6 +43,7 @@ Collection of some self-made helm-charts. Each top-level directory is a standalo
|
||||
- `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` — reusable workflow: `helm package`, then `helm push` to the private JK-Registry. Registry credentials come from 1Password Connect (`OP_CONNECT_TOKEN` secret, items under `op://GitHub-Actions/`). The loaded values are read via **`steps.<id>.outputs.*`**, mapped into each 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.JK_CR_* }}` 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: `-u` consumed the following `--password-stdin`, helm fell back to an interactive prompt and the job died with `inappropriate ioctl for device` — a terminal error for what was really an unset variable.
|
||||
- `change-app-version.yml` — manually dispatched app-version bump; the chart must be listed in its `chart-name` choice list.
|
||||
- `ci/helm-dependencies.sh <chart-dir>` — not a workflow but the step they all share: fetches a chart's declared dependencies into `charts/`, and returns silently for the charts that declare none (all but one). It runs in **five** places, because every helm command this repo builds on exits 1 while a declared dependency is missing: before lint and package in `validate-charts.yml`, before package in `publish-helm-chart.yml`, and inside `ci/run-install-test.sh`, `ci/run-quantity-render-test.sh` and `ci/run-trivy-config.sh`. See "Chart dependencies" in the style guidelines for what that costs.
|
||||
- `validate-charts.yml` — PR pipeline: detects which charts a PR touches and, for exactly those, runs `helm lint --strict`, `helm package`, and a chart-version bump check (`template-chart` is exempt from the version check). PRs without chart changes skip the build matrix. After the build stage, an **install-test** job (same changed-charts matrix) installs each changed chart into an ephemeral [k3d](https://k3d.io) cluster on the runner and requires every workload to become Ready — see below. Both matrices follow the changed chart directories, so a PR that only changes the harness under `ci/` exercises nothing by itself; the Trivy scan has a fixed-sample fallback for that case, the install-test has none — bump the affected chart's patch version to pull it into the matrix. The **resource-quantity render test** is the exception to that rule: it runs over every chart on every PR — see below.
|
||||
|
||||
### Resource-quantity render test (all charts, every PR)
|
||||
@@ -55,7 +57,7 @@ Collection of some self-made helm-charts. Each top-level directory is a standalo
|
||||
|
||||
The two cases exist because the bug behind #157 — quantities formatted with `printf "%v"`, where the `float64` the null-safe fallback helpers produce flips to exponential notation from about `1e6` — has a loud and a silent half. Loud: the reformatted quantity carries the suffix `e+06`, the helper rejects it, `helm template` exits non-zero. Silent: the *product* of a fractional `limitFactor` and a bare number is reformatted **after** the suffix check, so the render succeeds and the manifest carries `memory: "1.4999985e+06"` — rejected by nothing until the API server sees it at apply time. A check on the exit code alone therefore proves only half the case; the second case searches the **rendered text** for a resource quantity in exponential notation. `999999` stays below `1e6` on purpose, so the fraction case isolates the second formatting site instead of tripping over the first.
|
||||
|
||||
Why all charts rather than the changed ones: charts here have no dependencies, so the resources helper is copied into every chart's `templates/_helpers.tpl`. The regression arrives by copy — a new chart started from an older chart, or from `template-chart` before the fix — and in that PR the copied bug is in no diff anyone reads. Running over everything also removes the list nobody would maintain: adding a chart enrolls it automatically. Two renders per chart, no cluster, a few seconds for the whole repo; the job depends on nothing else and finishes well inside the `build-chart` runtime.
|
||||
Why all charts rather than the changed ones: the resources helper is copied into every chart's `templates/_helpers.tpl` rather than shared through a library chart. The regression arrives by copy — a new chart started from an older chart, or from `template-chart` before the fix — and in that PR the copied bug is in no diff anyone reads. Running over everything also removes the list nobody would maintain: adding a chart enrolls it automatically. Two renders per chart, no cluster, a few seconds for the whole repo; the job depends on nothing else and finishes well inside the `build-chart` runtime.
|
||||
|
||||
Two details worth knowing before touching it:
|
||||
|
||||
@@ -137,7 +139,8 @@ If the dependency dashboard reports `Failed to look up github-releases package
|
||||
1. Copy `template-chart/` to `<chart-name>/` and replace every `xxx` placeholder: template file names (`xxx.deployment.yaml` → `<app>.deployment.yaml`), `Chart.yaml`, the `values.yaml` keys, the `.Values.xxx.*` references inside the templates, **and both the `xxx.` helper prefixes and the values paths in `values.schema.json`**. Then walk the five points in "Values that must not lie" below — the schema and the null-safe fallbacks are copied with the template, but their content is chart-specific and wrong until you adapt it. The same goes for `templates/NOTES.txt`: it comes with the template as a worked example and is placeholder text until you replace it with what an operator of *your* app needs after the install (see "Everything else" below); its leading `{{/* … */}}` block explains the bar and is meant to be deleted.
|
||||
2. Add `.github/workflows/publish-<chart-name>.yml` (copy an existing one, e.g. `publish-outline.yml`).
|
||||
3. Add the chart name to the choice list in `.github/workflows/change-app-version.yml`.
|
||||
4. Add a `customManagers` block for the chart's main image in `renovate.json5` (copy a neighbouring one, swap `managerFilePatterns` and `depNameTemplate`) — without it the chart's app version is never updated. Check the image's tag list first: if it carries a flavour, date or prerelease suffix, add a `versioningTemplate` with a comment saying why.
|
||||
4. Add a `customManagers` block for the chart's main image in `renovate.json5` (copy a neighbouring one, swap `managerFilePatterns` and `depNameTemplate`) — without it the chart's app version is never updated. Check the image's tag list first: if it carries a flavour, date or prerelease suffix, add a `versioningTemplate` with a comment saying why. **Floating tags belong outside the pattern**: `ytdlp-web-player` anchors its versioning regex on the `v` prefix precisely so `latest` and `stable` — both of which move — can never be selected.
|
||||
5. Only if the chart needs a **dependency**: read "Chart dependencies" in the style guidelines first, it is a bar rather than a formality. Nothing has to be added to the pipelines — `ci/helm-dependencies.sh` already runs everywhere and keys off the `dependencies:` block — but the subchart's own updates then depend entirely on Renovate's built-in `helmv3` manager plus a chart-version bump, which is the part to write down at the pinned version.
|
||||
|
||||
Validate locally with:
|
||||
|
||||
@@ -201,7 +204,7 @@ These guidelines are **binding for every current and future chart** in this repo
|
||||
### 7. Resources (auto-limits)
|
||||
|
||||
- Resource requests ship as chart defaults in `values.yaml` and are user-overridable.
|
||||
- Limits are computed by a per-chart `_helpers.tpl` helper (reference implementation in `template-chart/templates/_helpers.tpl`): an explicitly set limit always wins, missing memory and ephemeral-storage limits default to `resources.limitFactor` × the request (factor default `3`), and a CPU limit is only rendered when set explicitly — otherwise CPU stays unlimited (it is compressible, throttling instead of OOM-killing). The helper is duplicated into every chart because charts have no dependencies.
|
||||
- Limits are computed by a per-chart `_helpers.tpl` helper (reference implementation in `template-chart/templates/_helpers.tpl`): an explicitly set limit always wins, missing memory and ephemeral-storage limits default to `resources.limitFactor` × the request (factor default `3`), and a CPU limit is only rendered when set explicitly — otherwise CPU stays unlimited (it is compressible, throttling instead of OOM-killing). The helper is copied into every chart rather than shared through a library chart — which would be a second artifact to publish, version and pull for a helper that changes about once a year; the resource-quantity render test above exists because a copy can carry an old bug into a diff nobody reads.
|
||||
|
||||
### 8. Secrets
|
||||
|
||||
@@ -277,4 +280,23 @@ Reference implementation: `template-chart/values.schema.json` and `template-char
|
||||
- `helm lint --strict` must pass without findings.
|
||||
- **Every chart ships a `templates/NOTES.txt`** (#43), `template-chart` included — so this is a requirement for a new chart, not a nice-to-have it can defer. No list of names here on purpose: a count goes stale the moment the next chart lands, which is exactly how the previous wording came to claim a number it never matched. Reference implementation: `template-chart/templates/NOTES.txt`, a placeholder-carrying template rather than finished prose, so a new chart adapts it instead of starting from an empty file. The bar it sets: print what an operator would otherwise have to look up in the templates (the rendered access URL, **which fields the 1Password item must carry**, the options actually active in this release, and the chart's known traps), conditional on the values really set. A `NOTES.txt` that only repeats the values is skimmed once and never read again. **No real domain anywhere** — the URL renders from `ingress.domain`, and where an example is unavoidable it is `example.com`. A `NOTES.txt` renders no Kubernetes object, so writing or reworking one is purely additive: a minor bump.
|
||||
- CI: every installable chart has fixtures under `ci/<chart>/` (`test-values.yaml`, optionally `fixtures.yaml`/`fixtures.env.yaml`/`settings.env`) or a justified entry in `ci/excluded-charts.txt` — the install-test fails if both are missing.
|
||||
- Charts have no dependencies (`charts/` directories are empty).
|
||||
### 14. Chart dependencies
|
||||
|
||||
Until `ytdlp-web-player` (#214) this section read "charts have no dependencies (`charts/` directories are empty)". That rule is gone, replaced by a bar rather than a ban — a dependency is **admissible, not free**, and the reason it stopped being a ban is worth keeping: the chart needed an oauth2-proxy in the data path in front of an application that ships no authentication at all, and proxy and app are only useful wired to each other.
|
||||
|
||||
**When a dependency earns its place.** All three, not two of three:
|
||||
|
||||
1. The dependent piece would otherwise be a second Helm release that only exists to serve this one, with the wiring between them living in neither chart.
|
||||
2. Upstream is the right owner of it. We consume their chart as it comes: **we do not patch it, do not re-harden it, do not re-implement its values.** Where their defaults fall short of the guidelines above, that is **documented in our `values.yaml`**, not fixed by us — and a values-level fix stays available to whoever installs it.
|
||||
3. It is **optional and off by default** (`condition:` in `Chart.yaml`), so the chart is complete without it and a default install renders none of its objects.
|
||||
|
||||
**What it costs, all of it visible before the next one is added:**
|
||||
|
||||
- **The subchart is invisible to everything downstream.** Its version is inside our packaged `.tgz`, so anything that watches Helm release versions — Fleet's `fleet.yaml` in `cluster-config` included — sees only *our* chart version. A new upstream release, security fix included, reaches a cluster **only** when we publish a new version of the outer chart. Nothing reports the gap. Renovate's built-in `helmv3` manager opens the PR (no `customManagers` entry needed — the repository URL and version sit together in one file) and `ci/renovate-bump-chart.sh` bumps the chart patch that ships it; that chain is the entire safety net. State this in a comment at the pinned version, not only in a review.
|
||||
- **Every pipeline that touches the chart needs a network fetch first.** `helm lint`, `helm package`, `helm template` and `helm install` all exit 1 while a declared dependency is missing from `charts/`, so `ci/helm-dependencies.sh` runs before each of them (see CI/CD above). One of those call sites is the resource-quantity render test, which runs over **every** chart on **every** PR — a dependency-carrying chart can therefore turn a third-party Helm repository's bad day into a red pipeline on a PR that touched nothing related.
|
||||
- **`charts/*.tgz` and `Chart.lock` are not committed** (`.gitignore` says why): the archive is a third-party binary nobody reviews in a diff, and a committed lock would arrive stale because Renovate rewrites `Chart.yaml` and cannot regenerate it — which is also why the pipelines run `helm dependency update` and not `helm dependency build`.
|
||||
- **The repo's own mechanisms stop at the subchart boundary.** `checksum/config`, the 1Password auto-restart annotation and the `scaleDown` flag reach our templates only. A chart that renders a `OnePasswordItem` for its subchart documents the manual `kubectl rollout restart` that replaces the missing restart-on-change.
|
||||
|
||||
Two implementation notes that cost time otherwise: a values key containing a dash is unreachable as `.Values.oauth2-proxy` (that parses as a subtraction) and needs `index .Values "oauth2-proxy"`, while `condition:` in `Chart.yaml` splits on dots only and needs no escaping. And inside a subchart `.Chart.Name` is the **subchart's** name — a value templated into it (`config.configFile` in this case) must spell the parent chart out literally; only `.Release.Name` is shared.
|
||||
|
||||
Charts that need no dependency still have none, and `charts/` stays empty for all of them. `template-chart` remains dependency-free: the scaffold should not carry a `dependencies:` block a new chart has to remember to delete.
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fetch a chart's declared dependencies into its charts/ directory.
|
||||
#
|
||||
# WHY THIS EXISTS (issue #214)
|
||||
#
|
||||
# Until ytdlp-web-player every chart here was dependency-free, so no pipeline
|
||||
# ever ran "helm dependency update" - and none of them tolerates a chart that
|
||||
# needs it. All three of the commands this repo builds on refuse to work while
|
||||
# a declared dependency is missing from charts/, each with its own wording:
|
||||
#
|
||||
# helm lint --strict [WARNING] chart directory is missing these dependencies: <name>
|
||||
# helm package Error: found in Chart.yaml, but missing in charts/ directory: <name>
|
||||
# helm template Error: An error occurred while checking for chart dependencies. ...
|
||||
#
|
||||
# All three exit 1, which is why this call sits in FIVE places rather than the
|
||||
# three the plan expected: the two build steps of validate-charts.yml, the
|
||||
# publish workflow - and the three ci/ scripts that render or install a chart
|
||||
# (install-test, quantity-render, trivy-config). The quantity-render one is the
|
||||
# easiest to overlook and the one that would have gone red first: it runs over
|
||||
# EVERY chart on EVERY pull request, so a dependency-carrying chart breaks it
|
||||
# from a PR that does not touch that chart at all.
|
||||
#
|
||||
# WHY "update" AND NOT "build"
|
||||
#
|
||||
# "helm dependency build" installs what Chart.lock names and fails when the
|
||||
# lock disagrees with Chart.yaml. Renovate rewrites the version in Chart.yaml
|
||||
# and cannot regenerate the lock, so every dependency PR would arrive red.
|
||||
# "update" resolves from Chart.yaml and writes the lock itself, which is also
|
||||
# why neither the lock nor the fetched .tgz is committed (see .gitignore).
|
||||
#
|
||||
# WHAT IT COSTS, NAMED HERE SO NOBODY HAS TO REDISCOVER IT
|
||||
#
|
||||
# A network fetch from a third-party Helm repository in every job that renders
|
||||
# the chart. That repository going away, or serving a version that no longer
|
||||
# exists, turns into a red pipeline on a PR that changed nothing related. This
|
||||
# is the price of the dependency, and it is the reason the README asks for one
|
||||
# to be justified rather than assumed.
|
||||
#
|
||||
# Charts without a "dependencies:" block are skipped without a word, so this is
|
||||
# safe to call unconditionally for every chart.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CHART_DIR="${1:?usage: ci/helm-dependencies.sh <chart-directory>}"
|
||||
|
||||
if [ ! -f "$CHART_DIR/Chart.yaml" ]; then
|
||||
echo "ci/helm-dependencies.sh: $CHART_DIR/Chart.yaml not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Deliberately a grep for the top-level key rather than a YAML parse: the
|
||||
# runners have no yq and no pyyaml (the same constraint ci/run-trivy-config.sh
|
||||
# and ci/run-quantity-render-test.sh work under). A commented-out block does
|
||||
# not match, because the anchor requires the key in column one.
|
||||
if ! grep -q '^dependencies:' "$CHART_DIR/Chart.yaml"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "==> helm dependency update $CHART_DIR"
|
||||
helm dependency update "$CHART_DIR"
|
||||
@@ -151,6 +151,10 @@ done
|
||||
echo "::endgroup::"
|
||||
|
||||
# --- 3. Install the chart ---------------------------------------------------
|
||||
# Same reason as in the lint and package steps: helm install refuses to touch a
|
||||
# chart whose declared dependencies are not in charts/. No-op for a chart
|
||||
# without dependencies; see ci/helm-dependencies.sh.
|
||||
"$ROOT/ci/helm-dependencies.sh" "$ROOT/$CHART"
|
||||
echo "::group::helm install"
|
||||
helm install "$RELEASE" "$ROOT/$CHART" \
|
||||
--namespace "$NAMESPACE" \
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
# case is not - it needs the rendered text to be searched for a quantity in
|
||||
# exponential notation. Both are checked here, per chart.
|
||||
#
|
||||
# Why over *all* charts instead of the changed ones: charts in this repo have
|
||||
# no dependencies, so the helper is copied into every chart's
|
||||
# templates/_helpers.tpl. The regression this guards against arrives by copy -
|
||||
# Why over *all* charts instead of the changed ones: the helper is copied into
|
||||
# every chart's templates/_helpers.tpl rather than shared through a library
|
||||
# chart. The regression this guards against arrives by copy -
|
||||
# a new chart started from an older chart, or from template-chart before the
|
||||
# fix. That chart is new to the repo, but its copy of the bug is not something
|
||||
# any diff of *this* PR would point at. The test is cheap enough (two renders
|
||||
@@ -111,6 +111,15 @@ for CHART in "${CHARTS[@]}"; do
|
||||
echo "ERROR: '$CHART' is not a chart directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# A chart with a declared dependency cannot be rendered at all while its
|
||||
# charts/ directory is empty - helm template exits 1 before it reaches a
|
||||
# single template. This job is the one that would notice first, because it
|
||||
# runs over EVERY chart on EVERY pull request: without this line, a PR
|
||||
# touching nothing but a README would go red on ytdlp-web-player. The script
|
||||
# skips charts that declare none, which is still all but one of them.
|
||||
"$ROOT/ci/helm-dependencies.sh" "$ROOT/$CHART"
|
||||
|
||||
SCHEMA="$ROOT/$CHART/values.schema.json"
|
||||
if [ ! -f "$SCHEMA" ]; then
|
||||
echo "::error title=quantity-render $CHART::$CHART has no values.schema.json - every chart in this repo ships one (README, \"Values that must not lie\"), and this test reads the resources paths from it."
|
||||
|
||||
@@ -92,6 +92,10 @@ if bad:
|
||||
PY
|
||||
|
||||
# --- 3. Render ---------------------------------------------------------------
|
||||
# helm template refuses to run at all while a declared dependency is missing
|
||||
# from charts/, so this has to happen before the render. No-op for a chart
|
||||
# without dependencies; see ci/helm-dependencies.sh.
|
||||
"$ROOT/ci/helm-dependencies.sh" "$ROOT/$CHART"
|
||||
echo "::group::helm template $CHART ($VALUES_LABEL)"
|
||||
# ${a[@]+"${a[@]}"} - an empty array under `set -u` is an error on bash 3.2
|
||||
# (the macOS default), where this script also runs locally.
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Install-test values for ytdlp-web-player.
|
||||
#
|
||||
# The chart needs exactly these two values for a default install: the app
|
||||
# itself has no secrets, no PVC and no external dependency to fake, so there is
|
||||
# no fixtures.yaml next to this file. example.com is a placeholder - nothing in
|
||||
# this repo may carry a production domain.
|
||||
ingress:
|
||||
domain: ytdlp-web-player.example.com
|
||||
clusterIssuer: letsencrypt
|
||||
|
||||
# THE PROXY STAYS OFF HERE. DO NOT "FIX" THIS BY ENABLING IT.
|
||||
#
|
||||
# Two independent reasons, either one sufficient:
|
||||
#
|
||||
# 1. It would test somebody else's chart. That the oauth2-proxy chart works
|
||||
# is a precondition of using it, and providing it is the upstream's job.
|
||||
# What this repo owns is the WIRING - the upstreams line that has to carry
|
||||
# the rendered Service name of this release - and wiring is a render-time
|
||||
# property, checked by rendering, not by installing. See the PR for #214
|
||||
# for that check.
|
||||
#
|
||||
# 2. It could not pass even if we wanted it to. oauth2-proxy fetches the OIDC
|
||||
# discovery document of its provider at startup and never becomes Ready
|
||||
# without one. There is no Authentik in the CI cluster, and standing one
|
||||
# up to watch a third-party chart start is a test of the fixture, not of
|
||||
# the chart.
|
||||
#
|
||||
# Switching it on therefore does not produce a stricter test - it produces a
|
||||
# permanently red install-test whose cause is outside this repository.
|
||||
oauth2-proxy:
|
||||
enabled: false
|
||||
@@ -97,6 +97,27 @@
|
||||
matchDepTypes: ["app-version"],
|
||||
addLabels: ["app-version"],
|
||||
},
|
||||
{
|
||||
description: [
|
||||
"Helm subchart dependencies (the `dependencies:` block of a Chart.yaml). Handled by",
|
||||
"the BUILT-IN helmv3 manager - no customManager needed, and none would help: the",
|
||||
"repository URL and the version sit next to each other in one file, which is the",
|
||||
"case the built-in manager exists for. It is enabled by config:recommended and this",
|
||||
"config sets no enabledManagers, so nothing has to switch it on. Verified by dry-run",
|
||||
"for #214: with ytdlp-web-player pinned one release back, the lookup returns",
|
||||
"oauth2-proxy 10.6.2 -> 10.7.0 as a manager=helmv3 minor update.",
|
||||
"",
|
||||
"This rule adds only a label. It exists because the update matters more here than",
|
||||
"its size suggests: a subchart is invisible to everything downstream that watches",
|
||||
"Helm release versions, so this PR is the ONLY way a new proxy version - a security",
|
||||
"fix included - ever reaches a cluster. Under the automerge rule above, patch and",
|
||||
"minor updates do that on their own once the pipeline is green, and",
|
||||
"ci/renovate-bump-chart.sh bumps the chart patch that publishes it (appVersion is",
|
||||
"unchanged, so it takes the patch branch). A major arrives as a PR to merge by hand.",
|
||||
],
|
||||
matchManagers: ["helmv3"],
|
||||
addLabels: ["subchart"],
|
||||
},
|
||||
{
|
||||
description: [
|
||||
"nginx numbers its release branches by minor: even is the stable branch, odd is",
|
||||
@@ -396,6 +417,32 @@
|
||||
datasourceTemplate: "docker",
|
||||
depTypeTemplate: "app-version",
|
||||
},
|
||||
{
|
||||
description: [
|
||||
"ytdlp-web-player: the registry carries exactly two kinds of tag - four versioned",
|
||||
"ones (v1.0.0, v1.0.1, v1.1.0, v1.2.0) and the two floating ones, 'latest' and",
|
||||
"'stable'. Both floating tags MOVE: 'stable' was republished at the same minute as",
|
||||
"v1.2.0 and 'latest' days later, so either of them as an appVersion would pin",
|
||||
"nothing and change the running image without a single commit.",
|
||||
"The anchored regex versioning below is what keeps them out - it requires the v",
|
||||
"prefix and three numeric parts, which neither 'latest' nor 'stable' can satisfy.",
|
||||
"docker versioning alone would probably have dropped them too, but 'probably' is",
|
||||
"the wrong basis for the value that selects the image.",
|
||||
],
|
||||
customType: "regex",
|
||||
managerFilePatterns: ["ytdlp-web-player/Chart.yaml"],
|
||||
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
|
||||
depNameTemplate: "docker.io/matszwe02/ytdlp_web_player",
|
||||
datasourceTemplate: "docker",
|
||||
depTypeTemplate: "app-version",
|
||||
// The "v" stays outside every capture group: it is part of the tag and
|
||||
// carries no ordering, and the same rule the searxng block documents
|
||||
// applies - a group is a claim about how the value is compared. The
|
||||
// anchors are what exclude latest/stable. Precedent for keeping the v in
|
||||
// appVersion at all: calibre-web-automated (+upv4.0.6) and matrix-synapse
|
||||
// (+upv1.159.0) - it is part of the tag, so it is part of appVersion.
|
||||
versioningTemplate: "regex:^v(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)$",
|
||||
},
|
||||
{
|
||||
customType: "regex",
|
||||
managerFilePatterns: ["zipline/Chart.yaml"],
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -0,0 +1,53 @@
|
||||
apiVersion: v2
|
||||
name: ytdlp-web-player
|
||||
description: A Helm chart to deploy ytdlp_web_player on Kubernetes, with an optional oauth2-proxy in front of it
|
||||
icon: https://raw.githubusercontent.com/matszwe02/ytdlp_web_player/main/src/static/favicon.svg
|
||||
|
||||
type: application
|
||||
|
||||
version: 1.0.0+upv1.2.0
|
||||
appVersion: "v1.2.0"
|
||||
|
||||
maintainers:
|
||||
- name: jklein
|
||||
|
||||
# The first chart in this repo with a dependency. The rule it breaks used to
|
||||
# read "charts have no dependencies (charts/ directories are empty)" and was
|
||||
# removed from CLAUDE.md and README.md in the same PR that added this chart -
|
||||
# see README "Chart style guidelines" for when a dependency is admissible and
|
||||
# what it costs.
|
||||
#
|
||||
# Why one here: the app ships no authentication of any kind, so anyone who
|
||||
# reaches its URL can drive it. The proxy is the answer, and it belongs into
|
||||
# this chart rather than next to it, because the two are only useful wired to
|
||||
# each other: the proxy's upstream is this release's Service, and the wiring is
|
||||
# the part that goes wrong silently.
|
||||
dependencies:
|
||||
- name: oauth2-proxy
|
||||
# KEEP THIS VERSION MOVING - it is the only path a proxy update has.
|
||||
#
|
||||
# In the cluster the proxy used to be a Helm release of its own, and the
|
||||
# update automation read its chart version straight out of fleet.yaml. As a
|
||||
# subchart it is invisible there: Fleet only ever sees the version of
|
||||
# ytdlp-web-player, and what this Chart.yaml declares is baked into the
|
||||
# packaged .tgz. So a new oauth2-proxy release - a security fix included -
|
||||
# reaches an operator ONLY when this line is raised and a new
|
||||
# ytdlp-web-player version is published. Nothing reports the gap; the proxy
|
||||
# simply stays where it is.
|
||||
#
|
||||
# Renovate's built-in helmv3 manager reads this block and opens the PR
|
||||
# (verified by dry-run, see the PR for #214); ci/renovate-bump-chart.sh then
|
||||
# bumps this chart's patch version, which is what actually ships it. That
|
||||
# chain is the whole safety net - if it is ever switched off, this line
|
||||
# needs a human on a calendar instead.
|
||||
#
|
||||
# The subchart version is independent of appVersion: raising it is a minor
|
||||
# or patch bump of this chart, never tied to the app image.
|
||||
version: "10.7.0"
|
||||
repository: "https://oauth2-proxy.github.io/manifests"
|
||||
# Off by default, so a plain install renders nothing of it at all. Helm
|
||||
# splits a condition on dots only, so the dash in the chart name needs no
|
||||
# escaping here - but every read of the same key inside a template does
|
||||
# need index .Values "oauth2-proxy", because .Values.oauth2-proxy parses as
|
||||
# a subtraction.
|
||||
condition: oauth2-proxy.enabled
|
||||
@@ -0,0 +1,243 @@
|
||||
{{- $replicas := include "ytdlp-web-player.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.ytdlpWebPlayer.replicas) -}}
|
||||
{{- $res := fromYaml (include "ytdlp-web-player.effectiveResources" .Values.ytdlpWebPlayer.resources) -}}
|
||||
{{- $eph := index ($res.limits | default dict) "ephemeral-storage" | default "the computed ephemeral-storage limit" -}}
|
||||
{{- $startup := fromYaml (include "ytdlp-web-player.startupProbe" .Values.ytdlpWebPlayer.startupProbe) -}}
|
||||
{{- $proxy := (index .Values "oauth2-proxy") | default dict -}}
|
||||
{{- $proxyIngress := $proxy.ingress | default dict -}}
|
||||
{{- $proxyHost := first ($proxyIngress.hosts | default list) -}}
|
||||
{{- $env := dict -}}
|
||||
{{- range $entry := .Values.ytdlpWebPlayer.env }}
|
||||
{{- if not (kindIs "invalid" $entry.value) }}{{- $_ := set $env $entry.name $entry.value }}{{- end }}
|
||||
{{- end }}
|
||||
ytdlp_web_player {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
||||
{{- if $proxy.enabled }} With the oauth2-proxy subchart enabled.{{ end }}
|
||||
|
||||
{{- if and $proxy.enabled $proxyIngress.enabled }}
|
||||
|
||||
ACCESS — THROUGH THE PROXY
|
||||
https://{{ $proxyHost | default "<oauth2-proxy.ingress.hosts is empty>" }}
|
||||
|
||||
The Ingress belongs to the proxy, the proxy forwards to
|
||||
{{ include "ytdlp-web-player.appServiceAddress" . }}, and that Service is the only
|
||||
way into the application. Certificate and TLS secret are the subchart's too —
|
||||
this chart's ingress.clusterIssuer does not reach it; set the cert-manager
|
||||
annotation under oauth2-proxy.ingress.annotations.
|
||||
{{- else 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.
|
||||
Both ingress.domain and ingress.clusterIssuer are unnecessary here — unlike
|
||||
in some other charts in this repo, nothing outside the Ingress template reads
|
||||
the domain: this application builds no absolute URLs and has no base-URL
|
||||
setting. Reach it inside the cluster:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80
|
||||
|
||||
Then open http://localhost:8080.
|
||||
{{- end }}
|
||||
{{ if eq $replicas "0" }}
|
||||
SCALED TO ZERO
|
||||
{{ if .Values.scaleDown }}scaleDown: true{{ else }}ytdlpWebPlayer.replicas: 0{{ end }} — the Deployment runs with replicas: 0, so nothing answers
|
||||
until you set it back and upgrade again. Service{{ if .Values.ingress.enabled }} and Ingress stay in place,
|
||||
so the certificate is not re-issued.{{ else }} stays in place.{{ end }} Nothing is lost that was not
|
||||
going to be lost anyway: the video cache is an emptyDir (see below).
|
||||
{{- if $proxy.enabled }}
|
||||
Note that scaleDown does NOT reach the subchart — the proxy keeps running and
|
||||
will answer with a gateway error. Set oauth2-proxy.replicaCount: 0 as well.
|
||||
{{- end }}
|
||||
{{ end }}
|
||||
{{- if and .Values.ingress.enabled (not $proxy.enabled) }}
|
||||
THIS RELEASE IS OPEN TO EVERYONE WHO KNOWS THE URL
|
||||
The application has NO authentication of any kind, and this chart adds none
|
||||
while oauth2-proxy.enabled is false. What an anonymous visitor can do here is
|
||||
not "read a page": they can make this pod fetch and transcode arbitrary
|
||||
videos from the internet — your egress, your CPU, your IP address in the
|
||||
logs of whatever site is being pulled from, and your ephemeral storage.
|
||||
|
||||
Assume it is found. Hosts get scanned, and certificate transparency logs
|
||||
publish every name cert-manager requests, so "nobody knows the URL" has a
|
||||
half-life of hours.
|
||||
|
||||
The intended fix is in this chart:
|
||||
|
||||
oauth2-proxy:
|
||||
enabled: true
|
||||
ingress:
|
||||
enabled: true
|
||||
hosts: ["{{ .Values.ingress.domain }}"]
|
||||
ingress:
|
||||
enabled: false # the proxy owns the host from then on
|
||||
|
||||
Anything else in front of it — an ingress-nginx auth annotation, a network
|
||||
policy, a VPN-only ingress class — has to come from ingress.annotations or
|
||||
from outside this chart.
|
||||
{{- end }}
|
||||
{{- if and $proxy.enabled .Values.ingress.enabled }}
|
||||
|
||||
WARNING — THE APP'S OWN INGRESS IS A WAY PAST THE PROXY
|
||||
Both oauth2-proxy.enabled and ingress.enabled are true. The chart only
|
||||
refuses this when both claim the SAME host; here they do not, so it renders —
|
||||
but https://{{ .Values.ingress.domain }} reaches the application directly, with no
|
||||
authentication in front of it, whatever the proxy does on its own host.
|
||||
Unless that is deliberate, set ingress.enabled=false.
|
||||
{{- end }}
|
||||
{{- if $proxy.enabled }}
|
||||
|
||||
THE PROXY IS A SUBCHART, AND THAT HAS TWO CONSEQUENCES
|
||||
1. Its version comes from THIS chart's Chart.yaml (oauth2-proxy {{ (first .Chart.Dependencies).Version | default "" }}) and is
|
||||
baked into the packaged chart. Nothing that watches Helm release versions
|
||||
can see it, so a new proxy release — a security fix included — arrives
|
||||
only when a new ytdlp-web-player version is released. There is no second
|
||||
place that would notice; do not wait for one.
|
||||
2. Its templates are upstream's. It does not follow this repo's hardening,
|
||||
probe and resource conventions, and we deliberately do not patch it. Give
|
||||
it values (oauth2-proxy.resources, .securityContext, …) if you want that
|
||||
shape.
|
||||
|
||||
SECRETS — WHAT THE 1PASSWORD ITEM MUST CARRY
|
||||
{{ if .Values.secrets.oauth2ProxySecretRef }}{{ .Values.secrets.oauth2ProxySecretRef }}{{ else }}(secrets.oauth2ProxySecretRef is not set){{ end }}
|
||||
→ Secret {{ .Release.Name }}-{{ .Chart.Name }}-oauth2-proxy-secret
|
||||
client-id the OIDC client id
|
||||
client-secret its secret
|
||||
cookie-secret 16, 24 or 32 bytes, URL-SAFE base64
|
||||
|
||||
The operator copies the item's FIELD NAMES into the Secret's keys, so those
|
||||
three spellings are exact. A field named differently does not fail this
|
||||
install — the Secret is written without that key and the proxy pod sits in
|
||||
CreateContainerConfigError, which looks like a broken chart.
|
||||
|
||||
Generate the cookie secret as
|
||||
|
||||
openssl rand -base64 32 | tr '+/' '-_'
|
||||
|
||||
The tr is not cosmetic: oauth2-proxy decodes the value as URL-safe base64
|
||||
first, so a standard-base64 value containing + or / fails to decode and the
|
||||
pod exits with "cookie_secret must be 16, 24, or 32 bytes".
|
||||
|
||||
Repointing secrets.oauth2ProxySecretRef at a different item does NOT restart
|
||||
the proxy — the subchart's pod template is unchanged by it, and Helm never
|
||||
sees a Secret's content anyway:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} rollout restart deployment/{{ .Release.Name }}-oauth2-proxy
|
||||
|
||||
THE PROVIDER IS THE ONLY GATE, AND IT IS WIDER THAN IT LOOKS
|
||||
The proxy is configured with email_domains = ["*"], so it admits every
|
||||
identity the provider hands it. Which people that is, is decided ENTIRELY by
|
||||
the group or policy binding on the provider application — and this app has no
|
||||
second login behind the proxy, unlike the other instances in the cluster. A
|
||||
forgotten or too-wide binding therefore does not mean "one more user", it
|
||||
means every account the provider knows. Check that binding when you set this
|
||||
up, not later.
|
||||
|
||||
Two settings in the default configFile are load-bearing; do not remove them
|
||||
while trimming:
|
||||
reverse_proxy = true else every redirect is built on http://
|
||||
behind the TLS ingress — a loop
|
||||
session_cookie_minimal = true else id_token and access_token go into the
|
||||
session cookie, a longer group list pushes
|
||||
it past 4kB, and the browser drops it: the
|
||||
symptom is HTTP 502 AFTER a successful
|
||||
login, with [AuthSuccess] in the log
|
||||
Its companion belongs on the proxy's Ingress:
|
||||
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
|
||||
|
||||
And leave alphaConfig.enabled false. With it on, the subchart generates a
|
||||
config WITHOUT upstreams and ignores config.configFile silently — the login
|
||||
succeeds and the app answers 404.
|
||||
|
||||
NOT MEASURED: HLS PLAYBACK THROUGH THE PROXY
|
||||
Rendering and wiring are verified; a long HLS stream through proxy and
|
||||
ingress is not. Three places to look first if playback stalls where a direct
|
||||
ingress works: proxy-buffering in ingress-nginx, its 60-second
|
||||
proxy-read/send timeouts, and the proxy's own upstream timeout. Browser
|
||||
extension and PWA are unverified too — an expired session answers with a
|
||||
redirect to the provider, not a 401, and a fetch() sees only an opaque
|
||||
redirect.
|
||||
{{- end }}
|
||||
|
||||
A READY POD DOES NOT MEAN A VIDEO WILL PLAY
|
||||
All three probes request "/", which renders the player page and touches
|
||||
neither the network nor ffmpeg. That is on purpose — it is the only cheap
|
||||
path the app has, and it keeps a slow upstream site from restarting the pod.
|
||||
The price is that egress problems are invisible to Kubernetes: with DNS
|
||||
broken or outbound traffic blocked, "/" still answers 200, the rollout goes
|
||||
green, and every playback fails. Check it from the outside instead:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} logs deployment/{{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
|
||||
A failing fetch shows up there as a yt-dlp error line, never as a probe
|
||||
failure. The startup budget is {{ $startup.periodSeconds }}s x {{ $startup.failureThreshold }} = {{ mul $startup.periodSeconds $startup.failureThreshold }}s, which only has to
|
||||
cover the first process start.
|
||||
|
||||
THE VIDEO CACHE IS AN emptyDir — AND IT IS AN EVICTION RISK, NOT A DISK
|
||||
DATA_PATH is /data, backed by an emptyDir. Two things follow:
|
||||
|
||||
Nothing survives. A restart, a node drain, an eviction or a helm upgrade
|
||||
empties it, and every cached video is fetched again on demand. That is the
|
||||
intended shape — the app expires files by itself after MAX_VIDEO_AGE — but do
|
||||
not treat it as storage.
|
||||
|
||||
An emptyDir counts against the container's ephemeral-storage limit
|
||||
({{ $eph }}){{ if (.Values.storage).dataSizeLimit }}, and storage.dataSizeLimit ({{ (.Values.storage).dataSizeLimit }}) caps it a second time{{ end }}. Going over it is not an
|
||||
application error: the kubelet evicts the whole pod, the reason sits on the
|
||||
pod rather than in any log, and everything in flight goes with it. Raising
|
||||
the limit is the wrong first move — the knobs that decide how much lands
|
||||
there are in ytdlpWebPlayer.env:
|
||||
|
||||
MAX_VIDEO_DURATION upstream default 36000 (ten hours, one enormous file)
|
||||
MAX_VIDEO_AGE upstream default 3600 (seconds after last access)
|
||||
MAX_PROCESSES upstream default 5 (concurrent ffmpeg/yt-dlp)
|
||||
|
||||
Currently set here: {{ if $env }}{{ range $k, $v := $env }}{{ $k }}={{ $v }} {{ end }}{{ else }}none — all upstream defaults apply{{ end }}
|
||||
|
||||
yt-dlp DOES NOT UPDATE ITSELF IN THIS CHART
|
||||
On start and periodically the app runs "pip install --upgrade yt-dlp" into
|
||||
its own site-packages. With readOnlyRootFilesystem: true that write cannot
|
||||
happen; the app catches the failure and carries on with the version baked
|
||||
into the image (verified — the container starts, serves and transcodes
|
||||
normally, the update attempt just does nothing).
|
||||
|
||||
This is the right trade in a cluster — a pod that pulls unpinned code off the
|
||||
network on every restart is not something to want — but know the
|
||||
consequence: when a video site changes its extraction and playback breaks,
|
||||
waiting does not fix it. The fix arrives as a new app image, i.e. a new
|
||||
appVersion of this chart. There is no in-place update path.
|
||||
|
||||
The tools' CACHE is a separate matter and is writable on purpose: /cache is
|
||||
an emptyDir, with HOME and XDG_CACHE_HOME pointed at it, holding deno's
|
||||
module cache and yt-dlp's JavaScript challenge-solver library. Without it
|
||||
the pod still works, but re-fetches that library from github.com on every
|
||||
extraction — so if egress is restricted, github.com belongs on the allow
|
||||
list either way, and doubly so if this volume is ever removed.
|
||||
|
||||
ytdlpWebPlayer.env IS APPENDED, NOT MERGED
|
||||
The entries land after DATA_PATH and PORT, which the chart sets itself. Do
|
||||
not set those two here: Kubernetes applies the LAST entry of the list, so a
|
||||
duplicate silently wins and points the app away from its writable volume or
|
||||
off the port that the Service and all three probes use. Everything else the
|
||||
upstream understands is fair game; src/example.env in the upstream repository
|
||||
is the authoritative list.
|
||||
{{- if gt (atoi $replicas) 1 }}
|
||||
|
||||
MORE THAN ONE REPLICA SHARES NOTHING
|
||||
ytdlpWebPlayer.replicas is {{ $replicas }}. Each pod has its own /data emptyDir, so a
|
||||
video fetched by one is invisible to the other, and without session affinity
|
||||
the segment requests of a single playback are spread across both — which
|
||||
looks like a stuttering or failing stream rather than an error. Pin clients
|
||||
to a pod at the ingress, or stay at one replica.
|
||||
{{- end }}
|
||||
|
||||
WHAT THIS CHART DELIBERATELY DOES NOT DO
|
||||
No cookies.txt: videos that require a login on the source site do not work,
|
||||
and there is no value to make them work. No PersistentVolume. No mirror of
|
||||
the upstream's twenty environment switches as chart values — they live in
|
||||
ytdlpWebPlayer.env, so an upstream release cannot make this chart lie.
|
||||
@@ -0,0 +1,372 @@
|
||||
{{/*
|
||||
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).
|
||||
|
||||
Usage (the values path to the resources dict varies per chart):
|
||||
resources:
|
||||
{{- include "ytdlp-web-player.resources" .Values.<app>.resources | nindent 12 }}
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix; template-chart/templates/_helpers.tpl is the
|
||||
reference implementation, and ci/run-quantity-render-test.sh renders every
|
||||
chart on every PR precisely because a copy can carry an old bug into a diff
|
||||
nobody reads. The copy is not a leftover of the old "charts have no
|
||||
dependencies" rule: a library chart would be a second thing to publish,
|
||||
version and pull for a helper that changes about once a year.
|
||||
*/}}
|
||||
{{- define "ytdlp-web-player.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 "ytdlp-web-player.resourceKey" $key) $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $limits := dict -}}
|
||||
{{- range $key, $value := ($resources.limits | default dict) -}}
|
||||
{{- if not (kindIs "invalid" $value) -}}
|
||||
{{- $_ := set $limits (include "ytdlp-web-player.resourceKey" $key) $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- range $key, $request := $requests -}}
|
||||
{{- if and (ne $key "cpu") (not (hasKey $limits $key)) -}}
|
||||
{{- $_ := set $limits $key (include "ytdlp-web-player.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 "ytdlp-web-player.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 "ytdlp-web-player.multiplyQuantity" -}}
|
||||
{{- $quantity := include "ytdlp-web-player.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 "ytdlp-web-player.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 "ytdlp-web-player.formatNumber" -}}
|
||||
{{- if kindIs "float64" . -}}
|
||||
{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%v" . -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render the replica count for a Deployment/StatefulSet.
|
||||
|
||||
Input: dict
|
||||
"scaleDown" the chart-global .Values.scaleDown flag (may be nil)
|
||||
"replicas" the workload's replicas value (may be nil)
|
||||
|
||||
Rules (issue #25):
|
||||
- scaleDown: true takes precedence and always yields 0.
|
||||
- otherwise an explicitly set replicas value is used as given, including an
|
||||
explicit 0 — Helm's "default" would swallow 0, hence the explicit
|
||||
kindIs "invalid" nil check.
|
||||
- a missing replicas value defaults to 1.
|
||||
|
||||
Usage (the values path to the workload's replicas varies per chart):
|
||||
replicas: {{ include "ytdlp-web-player.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.<app>.replicas) }}
|
||||
|
||||
NOTE: like the resources helper above, this is a copy carrying this chart's
|
||||
name as define prefix; template-chart holds the reference implementation.
|
||||
*/}}
|
||||
{{- define "ytdlp-web-player.replicas" -}}
|
||||
{{- if .scaleDown -}}
|
||||
0
|
||||
{{- else if not (kindIs "invalid" .replicas) -}}
|
||||
{{- .replicas -}}
|
||||
{{- else -}}
|
||||
1
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Merge a user-supplied values sub-tree onto a dict of chart defaults so that a
|
||||
null / empty override falls back to the defaults instead of erasing them.
|
||||
|
||||
Input: dict "defaults" <dict> "given" <dict|nil> "path" <string, optional
|
||||
values path used in error messages>. Output: YAML of the merged dict (consume
|
||||
it with fromYaml).
|
||||
|
||||
Why this exists (issues #61 and #99): Helm merges values maps recursively, but
|
||||
an override key written without children — e.g. a leftover
|
||||
|
||||
securityContext:
|
||||
|
||||
after deleting its last remaining sub-key — is YAML null. Where the chart
|
||||
defines a default, Helm treats that null as a deletion of the default; the
|
||||
template then renders "securityContext: null": no readOnlyRootFilesystem, no
|
||||
dropped capabilities, no allowPrivilegeEscalation: false. The workload starts
|
||||
and reports healthy while being unhardened, so nothing points at the mistake.
|
||||
The same shape erases a probe (it disappears) or the resources block (no
|
||||
requests, no computed limits). This helper defines the opposite semantics: an
|
||||
empty block means "chart defaults apply".
|
||||
|
||||
Rules:
|
||||
- a nil "given" is treated as an empty dict (all defaults apply).
|
||||
- a key whose override value is null keeps the default (nulls never delete).
|
||||
- a key present in both as a map is merged recursively, so a partially
|
||||
filled block only overrides the keys it actually sets.
|
||||
- a scalar override for a key the defaults define as a map is rejected with
|
||||
a message naming the values path, instead of failing later with a template
|
||||
field error.
|
||||
- any other set value wins as given, including an explicit false or 0 —
|
||||
Helm's "default" and sprig's "mergeOverwrite" would both swallow those
|
||||
zero values, hence the explicit kindIs "invalid" nil check.
|
||||
- keys only present in the override are passed through unchanged.
|
||||
|
||||
NOTE: like the helpers above, this is a copy carrying this chart's name as
|
||||
define prefix; template-chart holds the reference implementation.
|
||||
*/}}
|
||||
{{- define "ytdlp-web-player.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 "ytdlp-web-player.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.<app>.securityContext" can itself be erased, and indexing into nil
|
||||
inside a template is not an error but silently yields nothing.
|
||||
*/}}
|
||||
{{- define "ytdlp-web-player.subBlock" -}}
|
||||
{{- $block := .block -}}
|
||||
{{- if kindIs "invalid" $block -}}
|
||||
{{- $block = dict -}}
|
||||
{{- end -}}
|
||||
{{- if not (kindIs "map" $block) -}}
|
||||
{{- fail (printf "%s must be a mapping or null, got %s (%v)" .path (kindOf $block) $block) -}}
|
||||
{{- end -}}
|
||||
{{- toYaml (dict "value" (index $block .key)) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The effective security contexts, probes and resources (issue #99).
|
||||
|
||||
The defaults below MUST stay in sync with the corresponding blocks in
|
||||
values.yaml, which stays the documented, user-facing place for them; this copy
|
||||
is only the fallback for the case where an override has erased the block.
|
||||
|
||||
Two rules that these fallbacks exist to demonstrate (README, "Chart style
|
||||
guidelines"):
|
||||
|
||||
- Reproduce THIS chart's defaults, not the repo standard. Where a chart
|
||||
deviates on purpose (a root-starting image, a non-default limitFactor, a
|
||||
pinned uid), the fallback must reproduce the deviation. Restoring the
|
||||
"clean" standard here is not tidying, it is an outage - in one chart it
|
||||
would move the runtime uid from 1000 to 911 and make existing volume data
|
||||
inaccessible.
|
||||
- Carry the probe's CONTEXT, not just its timings. A probe default without
|
||||
its port name, Host header or exec command is worse than useless: in one
|
||||
chart the probe vanished entirely, in another it answered 403 and the pod
|
||||
never became Ready.
|
||||
*/}}
|
||||
{{- define "ytdlp-web-player.podSecurityContext" -}}
|
||||
{{- $given := (fromYaml (include "ytdlp-web-player.subBlock" (dict "block" . "key" "pod" "path" "ytdlpWebPlayer.securityContext"))).value -}}
|
||||
{{- $defaults := dict
|
||||
"runAsNonRoot" true
|
||||
"runAsUser" 20000
|
||||
"runAsGroup" 1000
|
||||
"fsGroup" 1000
|
||||
"fsGroupChangePolicy" "Always"
|
||||
"seccompProfile" (dict "type" "RuntimeDefault") -}}
|
||||
{{- include "ytdlp-web-player.defaultedDict" (dict "defaults" $defaults "given" $given "path" "ytdlpWebPlayer.securityContext.pod") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "ytdlp-web-player.containerSecurityContext" -}}
|
||||
{{- $given := (fromYaml (include "ytdlp-web-player.subBlock" (dict "block" . "key" "container" "path" "ytdlpWebPlayer.securityContext"))).value -}}
|
||||
{{- $defaults := dict
|
||||
"allowPrivilegeEscalation" false
|
||||
"readOnlyRootFilesystem" true
|
||||
"runAsNonRoot" true
|
||||
"capabilities" (dict "drop" (list "ALL") "add" (list)) -}}
|
||||
{{- include "ytdlp-web-player.defaultedDict" (dict "defaults" $defaults "given" $given "path" "ytdlpWebPlayer.securityContext.container") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "ytdlp-web-player.livenessProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (dict "path" "/" "port" "http")
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 3 -}}
|
||||
{{- include "ytdlp-web-player.defaultedDict" (dict "defaults" $defaults "given" . "path" "ytdlpWebPlayer.livenessProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "ytdlp-web-player.readinessProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (dict "path" "/" "port" "http")
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 3 -}}
|
||||
{{- include "ytdlp-web-player.defaultedDict" (dict "defaults" $defaults "given" . "path" "ytdlpWebPlayer.readinessProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "ytdlp-web-player.startupProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (dict "path" "/" "port" "http")
|
||||
"periodSeconds" 5
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 30 -}}
|
||||
{{- include "ytdlp-web-player.defaultedDict" (dict "defaults" $defaults "given" . "path" "ytdlpWebPlayer.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.
|
||||
*/}}
|
||||
{{- define "ytdlp-web-player.effectiveResources" -}}
|
||||
{{- $defaults := dict
|
||||
"limitFactor" 3
|
||||
"requests" (dict "cpu" 0.5 "memory" "512Mi" "ephemeral-storage" "2Gi") -}}
|
||||
{{- $merged := fromYaml (include "ytdlp-web-player.defaultedDict" (dict "defaults" $defaults "given" . "path" "ytdlpWebPlayer.resources")) -}}
|
||||
{{- include "ytdlp-web-player.resources" $merged -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The application's listening port, taken from the container port named "http".
|
||||
|
||||
Deliberately NOT null-safe, unlike the probe and resources helpers above.
|
||||
Issue #99 covers the silent class of erased values - a probe that vanishes, a
|
||||
container that renders unhardened. This one is loud: a missing port yields a
|
||||
Service pointing nowhere and an application told to listen on "", which fails
|
||||
at once and in the open. An error that shows itself needs no fallback, and
|
||||
every extra branch has to be read by someone.
|
||||
|
||||
Because of this helper the number lives in exactly one place: it becomes the
|
||||
PORT environment variable, while the Service and all three probes address the
|
||||
same entry by NAME.
|
||||
*/}}
|
||||
{{- define "ytdlp-web-player.port" -}}
|
||||
{{- $port := "" -}}
|
||||
{{- range (.Values.ytdlpWebPlayer.ports | default list) -}}
|
||||
{{- if eq (.name | default "") "http" -}}
|
||||
{{- $port = .containerPort -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if or (kindIs "invalid" $port) (eq (printf "%v" $port) "") -}}
|
||||
{{- fail "ytdlpWebPlayer.ports must contain an entry named \"http\" carrying a containerPort - it is the port the application is told to listen on (PORT), the port the Service targets, and the port all three probes address" -}}
|
||||
{{- end -}}
|
||||
{{- printf "%v" $port -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The in-cluster address of this release's application Service - the address the
|
||||
oauth2-proxy subchart has to carry in its upstreams line.
|
||||
|
||||
A helper so that NOTES.txt and the default in values.yaml cannot drift apart.
|
||||
Note what it may NOT be built from on the subchart side: inside the subchart
|
||||
.Chart.Name is "oauth2-proxy", so the parent chart's name has to be written out
|
||||
literally there. Only .Release.Name is shared by both.
|
||||
*/}}
|
||||
{{- define "ytdlp-web-player.appServiceAddress" -}}
|
||||
http://{{ .Release.Name }}-{{ .Chart.Name }}-service:80
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Refuse to render two Ingresses for the same host (issue #214).
|
||||
|
||||
With the proxy enabled, this chart's Ingress and the subchart's Ingress can
|
||||
both be switched on, and both then claim the same host. That is not a merge and
|
||||
not a warning: ingress-nginx's admission webhook rejects whichever arrives
|
||||
second with "host ... and path ... is already defined in ingress ...". Under
|
||||
Fleet the bundle goes to ErrApplied and Fleet does not retry a rejected apply
|
||||
by itself - getting out costs a forceSyncGeneration bump on the GitRepo.
|
||||
Failing here costs a re-render.
|
||||
|
||||
Host-based rather than "both switched on", because a proxy on a host of its own
|
||||
collides with nothing. That case is not silently blessed either: NOTES.txt says
|
||||
plainly that the app's own Ingress is then a way past the proxy.
|
||||
|
||||
Called from the Deployment - the one template that always renders - so the
|
||||
guard cannot be skipped by scaling to zero or by rendering a single file with
|
||||
"helm template -s".
|
||||
*/}}
|
||||
{{- define "ytdlp-web-player.validate" -}}
|
||||
{{- $proxy := (index .Values "oauth2-proxy") | default dict -}}
|
||||
{{- $proxyIngress := $proxy.ingress | default dict -}}
|
||||
{{- if and .Values.ingress.enabled $proxy.enabled $proxyIngress.enabled -}}
|
||||
{{- if has .Values.ingress.domain ($proxyIngress.hosts | default list) -}}
|
||||
{{- fail (printf "ingress.enabled and oauth2-proxy.ingress.enabled both claim the host %q. ingress-nginx allows only one Ingress per host and path; its admission webhook rejects the second one, which leaves a Fleet bundle in ErrApplied until forceSyncGeneration is bumped. Decide who owns the host: with the proxy in the data path set ingress.enabled=false (the app is then reached through the proxy and its Service), or drop the proxy's Ingress with oauth2-proxy.ingress.enabled=false." .Values.ingress.domain) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,135 @@
|
||||
{{- include "ytdlp-web-player.validate" . }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
spec:
|
||||
replicas: {{ include "ytdlp-web-player.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.ytdlpWebPlayer.replicas) }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
template:
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
# NO checksum/config ANNOTATION HERE, AND NOTHING IS MISSING (#82).
|
||||
#
|
||||
# The rule is "every pod template consuming a ConfigMap carries a hash of
|
||||
# it". This workload consumes none: the application is configured purely
|
||||
# through environment variables, which are part of the pod template
|
||||
# already, so a changed value rolls the pods by itself. The chart renders
|
||||
# no ConfigMap and no Secret for the app - it has no credentials of its
|
||||
# own - so there is nothing to hash and no silent-stale-config gap to
|
||||
# close. Written down because an absent annotation otherwise looks like
|
||||
# an oversight on the next review round.
|
||||
#
|
||||
# The one Secret this chart can render, the oauth2-proxy OnePasswordItem,
|
||||
# is consumed by the SUBCHART's pod template, which this chart cannot
|
||||
# annotate. See values.yaml for the manual rollout restart that replaces
|
||||
# it.
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
{{- include "ytdlp-web-player.podSecurityContext" .Values.ytdlpWebPlayer.securityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
# Fully qualified on purpose: an unqualified "matszwe02/ytdlp_web_player"
|
||||
# resolves to Docker Hub implicitly, which is how the repo accumulated
|
||||
# its unqualified images.
|
||||
image: "docker.io/matszwe02/ytdlp_web_player:{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
# The four variables the chart owns.
|
||||
#
|
||||
# DATA_PATH is absolute, which the application explicitly allows (it
|
||||
# runs the value through os.path.abspath), so nothing has to be
|
||||
# writable below /app and the root filesystem stays read-only.
|
||||
#
|
||||
# PORT follows the container port named "http", so the Service, the
|
||||
# probes and the application cannot drift apart.
|
||||
#
|
||||
# HOME and XDG_CACHE_HOME point the cache of the two bundled tools
|
||||
# at the writable volume below. They are set rather than relying on
|
||||
# a mount at /.cache so that the path does not depend on which uid
|
||||
# the pod ends up running as - see values.yaml under storage.
|
||||
- name: DATA_PATH
|
||||
value: "/data"
|
||||
- name: PORT
|
||||
value: {{ include "ytdlp-web-player.port" . | quote }}
|
||||
- name: HOME
|
||||
value: "/cache"
|
||||
- name: XDG_CACHE_HOME
|
||||
value: "/cache"
|
||||
{{- range $value := .Values.ytdlpWebPlayer.env }}
|
||||
{{- if $value.value }}
|
||||
- name: {{ $value.name | quote }}
|
||||
# quote is required, not cosmetic: a value like "5000" or "true"
|
||||
# renders unquoted as a YAML number/bool, and the API server then
|
||||
# rejects the whole manifest with "cannot unmarshal number into
|
||||
# Go struct field EnvVar.value of type string".
|
||||
value: {{ tpl $value.value $ | 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:
|
||||
{{- toYaml .Values.ytdlpWebPlayer.ports | nindent 12 }}
|
||||
volumeMounts:
|
||||
# Exactly two, and each was measured on its own rather than added
|
||||
# as a precaution - see values.yaml under storage. Note in
|
||||
# particular that /tmp is NOT here: the usual reflex for a
|
||||
# read-only root filesystem, and this image does not need it.
|
||||
- name: data
|
||||
mountPath: /data
|
||||
- name: cache
|
||||
mountPath: /cache
|
||||
# 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 "ytdlp-web-player.livenessProbe" .Values.ytdlpWebPlayer.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- include "ytdlp-web-player.readinessProbe" .Values.ytdlpWebPlayer.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- include "ytdlp-web-player.startupProbe" .Values.ytdlpWebPlayer.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "ytdlp-web-player.effectiveResources" .Values.ytdlpWebPlayer.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- include "ytdlp-web-player.containerSecurityContext" .Values.ytdlpWebPlayer.securityContext | nindent 12 }}
|
||||
volumes:
|
||||
# The "emptyDir: {}" branches are written out rather than folded into
|
||||
# one block with an optional key: an emptyDir whose body renders empty
|
||||
# is YAML null, and a volume without a source is rejected by the API
|
||||
# server with "must specify a volume type" - after a green render.
|
||||
- name: data
|
||||
{{- if (.Values.storage).dataSizeLimit }}
|
||||
emptyDir:
|
||||
sizeLimit: {{ (.Values.storage).dataSizeLimit }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
- name: cache
|
||||
{{- if (.Values.storage).cacheSizeLimit }}
|
||||
emptyDir:
|
||||
sizeLimit: {{ (.Values.storage).cacheSizeLimit }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
restartPolicy: Always
|
||||
@@ -0,0 +1,29 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- include "ytdlp-web-player.validate" . }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided" .Values.ingress.clusterIssuer }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
rules:
|
||||
- host: {{ required "A Domain/Host must be provided" .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,26 @@
|
||||
{{- $proxy := (index .Values "oauth2-proxy") | default dict -}}
|
||||
{{- $config := $proxy.config | default dict -}}
|
||||
{{- $ownSecret := printf "%s-%s-oauth2-proxy-secret" .Release.Name .Chart.Name -}}
|
||||
{{- if and $proxy.enabled (eq (tpl ($config.existingSecret | default "") .) $ownSecret) }}
|
||||
# The three credentials the proxy needs, as a 1Password item (README, guideline
|
||||
# 8). This chart renders the CR itself rather than only expecting a Secret name,
|
||||
# for the same reason every other chart here does: the operator is the repo's
|
||||
# secret mechanism, and a chart that only names a Secret leaves the operator
|
||||
# half of the wiring to a second, undocumented place.
|
||||
#
|
||||
# It is rendered only while the subchart still points at THIS name. Set
|
||||
# oauth2-proxy.config.existingSecret to a Secret of your own and the chart
|
||||
# creates nothing here - your Secret then has to carry the same three keys.
|
||||
#
|
||||
# Note what the operator does and does not do: it copies the item's FIELD NAMES
|
||||
# into the Secret's keys. A field spelled differently produces a Secret without
|
||||
# that key, the install stays green, and the proxy pod sits in
|
||||
# CreateContainerConfigError. See values.yaml for the three names and for the
|
||||
# URL-safe base64 trap on cookie-secret.
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: {{ $ownSecret }}
|
||||
spec:
|
||||
itemPath: {{ required "The oauth2-proxy needs its credentials: set secrets.oauth2ProxySecretRef to the op:// path of an item carrying client-id, client-secret and cookie-secret - or point oauth2-proxy.config.existingSecret at a Secret you manage yourself." .Values.secrets.oauth2ProxySecretRef }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
spec:
|
||||
selector:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
# By name, so the container port is configured in exactly one place
|
||||
# (ytdlpWebPlayer.ports).
|
||||
targetPort: http
|
||||
name: http
|
||||
type: ClusterIP
|
||||
@@ -0,0 +1,196 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||
"title": "ytdlp-web-player values",
|
||||
"description": "additionalProperties is false on every level this chart owns, so a key the chart does not read is rejected at render time instead of being silently ignored (#68). Two kinds of object stay open: the ones passed verbatim into the pod spec (probes, security contexts, ports, free-form annotations), because Kubernetes owns their schema - and the whole oauth2-proxy block, because that schema belongs to the subchart. Closing the latter would mean re-declaring somebody else's values and rejecting every key they add next release.",
|
||||
"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 - and note that with a subchart present this is no longer hypothetical: whatever is set here reaches oauth2-proxy too.",
|
||||
"type": "object"
|
||||
},
|
||||
"scaleDown": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"secrets": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"oauth2ProxySecretRef": {
|
||||
"description": "op:// item path for the proxy credentials; the item must carry the fields client-id, client-secret and cookie-secret. Read only while the proxy is enabled and still points at the Secret this chart renders.",
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ytdlpWebPlayer": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"replicas": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"resources": {
|
||||
"$ref": "#/definitions/resources"
|
||||
},
|
||||
"livenessProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"readinessProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"startupProbe": {
|
||||
"$ref": "#/definitions/probe"
|
||||
},
|
||||
"securityContext": {
|
||||
"$ref": "#/definitions/securityContext"
|
||||
},
|
||||
"ports": {
|
||||
"description": "Rendered verbatim as the container's ports list; kept open because the chart does not interpret the entries. It does read one thing out of them: the entry named \"http\" carries the port that becomes PORT, the Service target and every probe target. A list without it fails the render with that message.",
|
||||
"type": ["array", "null"]
|
||||
},
|
||||
"env": {
|
||||
"$ref": "#/definitions/env"
|
||||
}
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"description": "The two emptyDirs. Deliberately not null-safe: an erased block means both size caps are unset, which is also the default, so nothing silent can happen here.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"dataSizeLimit": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"cacheSizeLimit": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ingress": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Gates this chart's whole Ingress template (#93). Here it frees BOTH domain and clusterIssuer: nothing outside the Ingress template reads either of them - the application needs no hostname of its own, so the searxng/matrix-synapse case of a domain read from an env block does not arise (guideline 1, checked in templates/ and in values.yaml).",
|
||||
"type": "boolean"
|
||||
},
|
||||
"clusterIssuer": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"domain": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"className": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"annotations": {
|
||||
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open.",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"oauth2-proxy": {
|
||||
"description": "Values of the oauth2-proxy subchart, passed straight through. Open on purpose and at every level: this chart owns neither the keys nor their meaning, and a closed copy would reject whatever the subchart adds in its next release - the exact failure mode a schema is supposed to prevent, only pointed at the wrong chart. This chart reads exactly three of them: enabled (the dependency condition), ingress.enabled and ingress.hosts (the two-Ingress guard).",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"resources": {
|
||||
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (#99) instead of rendering a container without resource accounting.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"limitFactor": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"requests": {
|
||||
"$ref": "#/definitions/resourceList"
|
||||
},
|
||||
"limits": {
|
||||
"$ref": "#/definitions/resourceList"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourceList": {
|
||||
"description": "Both spellings of the ephemeral-storage key are accepted because the resources helper normalizes camelCase to the Kubernetes key.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"cpu": {
|
||||
"type": ["string", "number", "null"]
|
||||
},
|
||||
"memory": {
|
||||
"type": ["string", "number", "null"]
|
||||
},
|
||||
"ephemeral-storage": {
|
||||
"type": ["string", "number", "null"]
|
||||
},
|
||||
"ephemeralStorage": {
|
||||
"type": ["string", "number", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
},
|
||||
"valueFrom": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"secretKeyRef": {
|
||||
"$ref": "#/definitions/keyRef"
|
||||
},
|
||||
"configMapKeyRef": {
|
||||
"$ref": "#/definitions/keyRef"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"keyRef": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "key"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityContext": {
|
||||
"description": "May be null / empty at any level: the chart then falls back to its hardening defaults (#99) instead of rendering an unhardened workload. The fallback reproduces THIS chart's defaults, including its pinned non-root uid for a root-by-default image (guideline 4).",
|
||||
"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 (#99) rather than the probe disappearing. The defaults carry the probe's context - the port NAME \"http\", so a changed container port moves the probes with it.",
|
||||
"type": ["object", "null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
# 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. Services and Ingress
|
||||
# stay in place; scaleDown: false brings the workloads back up.
|
||||
#
|
||||
# It does NOT reach into the oauth2-proxy subchart: that chart has its own
|
||||
# replicaCount, and a value of this chart cannot override it implicitly. With
|
||||
# the proxy enabled, scaling the whole release down means setting
|
||||
# oauth2-proxy.replicaCount: 0 as well.
|
||||
scaleDown: false
|
||||
|
||||
secrets:
|
||||
# op:// item path for the oauth2-proxy credentials. Read ONLY while the proxy
|
||||
# is enabled and still points at the Secret this chart creates for it (see
|
||||
# oauth2-proxy.config.existingSecret below); a plain install without the
|
||||
# proxy needs no 1Password item at all.
|
||||
#
|
||||
# The item must carry these three fields, spelled exactly like this, because
|
||||
# the operator copies an item's field names into the Secret's keys and the
|
||||
# subchart reads them by name:
|
||||
#
|
||||
# client-id the OIDC client id of the provider application
|
||||
# client-secret its client secret
|
||||
# cookie-secret 16, 24 or 32 bytes, URL-SAFE base64
|
||||
#
|
||||
# The cookie-secret is the one that costs an evening. oauth2-proxy decodes it
|
||||
# as URL-safe base64 FIRST, so the obvious "openssl rand -base64 32" produces
|
||||
# a value containing + and /, the decode fails, and the pod exits with
|
||||
# "cookie_secret must be 16, 24, or 32 bytes". Generate it as:
|
||||
#
|
||||
# openssl rand -base64 32 | tr '+/' '-_'
|
||||
#
|
||||
# A field spelled differently does not fail the install: the operator writes
|
||||
# a Secret without that key and the proxy pod stays in
|
||||
# CreateContainerConfigError, which looks like a broken chart.
|
||||
oauth2ProxySecretRef: null
|
||||
|
||||
ytdlpWebPlayer:
|
||||
# Replica count for this workload; 0 is allowed and scales it down.
|
||||
# The chart-global scaleDown flag takes precedence.
|
||||
#
|
||||
# More than one replica is possible but shares nothing: each pod keeps its
|
||||
# own /data emptyDir, so a video fetched by one pod is invisible to the
|
||||
# other, and without session affinity the HLS segment requests of a single
|
||||
# playback are spread over both. Raise it only behind an ingress that pins a
|
||||
# client to a pod.
|
||||
replicas: 1
|
||||
# DOCUMENTED DEVIATION FROM THE HARDENING BASELINE - runAsUser/runAsGroup.
|
||||
#
|
||||
# The upstream image carries no USER instruction and therefore runs as root
|
||||
# by default (verified in the published Dockerfile for v1.2.0: python:3.13-
|
||||
# alpine, apk add ffmpeg deno, no USER). That is a default, not a
|
||||
# requirement: measured, the container runs as an arbitrary uid with
|
||||
# readOnlyRootFilesystem: true and the full hardening baseline, serving the
|
||||
# UI and completing a real transcode. Everything it writes goes below
|
||||
# DATA_PATH and into the cache directory, and both are emptyDirs this chart
|
||||
# mounts writable (see storage). So the chart pins a non-root uid rather than
|
||||
# following the image, and this deviation is about the NUMBER only - no part
|
||||
# of the hardening baseline is relaxed.
|
||||
#
|
||||
# The uid is arbitrary on purpose (20000, the repo default). Nothing in the
|
||||
# image belongs to it - /app is world-readable and read-only here - and no
|
||||
# PersistentVolume is involved, so no data is tied to the number.
|
||||
securityContext:
|
||||
pod:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 20000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
fsGroupChangePolicy: Always
|
||||
# RuntimeDefault applies the container runtime's default seccomp filter,
|
||||
# which blocks the syscalls a normal workload never makes. It sits in the
|
||||
# pod context on purpose: from here it covers every container in the pod
|
||||
# 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: []
|
||||
# The application exposes no health endpoint, so all three probes use "/".
|
||||
#
|
||||
# What that path costs: it is Flask's index route, which renders the player
|
||||
# template and touches neither ffmpeg, yt-dlp nor the network. Measured in
|
||||
# k3d it answers in single-digit milliseconds, so probing it every 10s is
|
||||
# free even while transcoding runs.
|
||||
#
|
||||
# What a green probe does NOT prove: that any video can be played. The app
|
||||
# fetches its content from the internet at request time; with egress blocked
|
||||
# or DNS broken, "/" still answers 200 while every playback fails. See
|
||||
# NOTES.txt.
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
# The startup probe gates liveness/readiness, so those need no
|
||||
# initialDelaySeconds. Startup budget = 5s x 30 = 150s, which is generous:
|
||||
# the app opened its port in about two seconds in k3d, and the budget only
|
||||
# has to cover a slow image pull's first process start.
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 30
|
||||
# Extra environment variables, APPENDED to the ones the chart sets itself
|
||||
# (DATA_PATH and PORT). Values are rendered through tpl; the value,
|
||||
# secretKeyRef and configMapKeyRef forms are supported.
|
||||
#
|
||||
# This is where the application is configured. The upstream offers around
|
||||
# twenty switches and this chart deliberately mirrors none of them as its own
|
||||
# values: a chart that rebuilds every option is a lie one upstream release
|
||||
# later, and it would also have to be kept in values.schema.json. The full,
|
||||
# authoritative list is src/example.env in the upstream repository. As of
|
||||
# v1.2.0 it holds APP_TITLE, THEME_COLOR, GENERATE_SPRITE_BELOW, AMOLED_BG,
|
||||
# MAX_VIDEO_AGE, MAX_VIDEO_DURATION, DEFAULT_QUALITY, MAX_QUALITY, AUTOPLAY,
|
||||
# MIN_LIVE_BUFFER, ALWAYS_TRANSCODE, DISABLE_TRANSCODING, MAX_PROCESSES,
|
||||
# AUTOSKIP_SB_SEGMENTS, COOKIES_ONLY_ON_FAILURE, PLAYLIST_SUPPORT,
|
||||
# AUTO_BG_PLAYBACK, AUDIO_VISUALIZER and PROXY.
|
||||
#
|
||||
# Do not set DATA_PATH, PORT, HOME or XDG_CACHE_HOME here: those four are the
|
||||
# chart's, and a second entry with the same name silently wins (Kubernetes
|
||||
# applies the LAST entry of the list) - pointing the app away from its
|
||||
# writable volume, off the port the Service and probes use, or its caches
|
||||
# back onto the read-only root filesystem.
|
||||
#
|
||||
# Example:
|
||||
# env:
|
||||
# - name: APP_TITLE
|
||||
# value: "Player"
|
||||
# - name: MAX_PROCESSES
|
||||
# value: "2"
|
||||
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".
|
||||
#
|
||||
# No CPU limit on purpose (and this is the chart where it matters most):
|
||||
# transcoding is what this app does, ffmpeg takes every core it is given,
|
||||
# and a CPU limit does not fail a request - it throttles the encoder, which
|
||||
# arrives at the viewer as a stuttering stream with nothing in any log.
|
||||
#
|
||||
# ephemeral-storage is the interesting number here, not memory. Downloaded
|
||||
# and transcoded videos live in the /data emptyDir, and an emptyDir counts
|
||||
# against the container's ephemeral-storage LIMIT. Going over it does not
|
||||
# produce an application error: the kubelet evicts the whole pod, the
|
||||
# reason sits on the pod rather than in the app's log, and every video in
|
||||
# flight is gone with the emptyDir.
|
||||
#
|
||||
# Raising this number is the wrong first move. The knobs that decide how
|
||||
# much lands there are MAX_VIDEO_AGE (how long a file survives after the
|
||||
# last access), MAX_VIDEO_DURATION (upstream default 36000s = 10 hours, an
|
||||
# enormous single file) and MAX_PROCESSES (how many can arrive at once) -
|
||||
# all three via ytdlpWebPlayer.env. The limit only decides how the excess
|
||||
# is discovered.
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 0.5
|
||||
memory: 512Mi
|
||||
# 2Gi request -> 6Gi limit at limitFactor 3.
|
||||
ephemeralStorage: 2Gi
|
||||
ports:
|
||||
# The port the application listens on, and the single place it is
|
||||
# configured: the chart derives the PORT environment variable from the
|
||||
# entry named "http", the Service targets it by that name, and all three
|
||||
# probes address it by name too. Changing the number here moves all four
|
||||
# together - which is why the chart sets PORT itself instead of leaving it
|
||||
# to ytdlpWebPlayer.env, where it could disagree with this list.
|
||||
#
|
||||
# 5000 is the upstream default (EXPOSE 5000) and unprivileged, so the
|
||||
# AVD-KSV-0117 rule about containerPort < 1024 does not apply.
|
||||
- containerPort: 5000
|
||||
protocol: TCP
|
||||
name: http
|
||||
|
||||
storage:
|
||||
# DATA_PATH is set to /data and backed by an emptyDir: the app treats the
|
||||
# directory as a cache and removes videos again after MAX_VIDEO_AGE, so there
|
||||
# is nothing here worth a PersistentVolume - and nothing that survives a pod
|
||||
# restart either. That is deliberate; see NOTES.txt.
|
||||
#
|
||||
# Optional size caps for the two emptyDirs. Leave them null and the only
|
||||
# ceiling is the container's ephemeral-storage limit (see resources above);
|
||||
# note that the outcome of hitting either is the same - the kubelet evicts
|
||||
# the pod - so a sizeLimit buys a clearer eviction message, not a graceful
|
||||
# failure.
|
||||
dataSizeLimit: null
|
||||
|
||||
# The second emptyDir, at /cache, with HOME and XDG_CACHE_HOME pointed at it.
|
||||
#
|
||||
# EVERY MOUNT HERE WAS MEASURED SEPARATELY, and the result is not the one the
|
||||
# reflex predicts. Two runs of the image at uid 20000 with a read-only root
|
||||
# filesystem, driven through metadata extraction, HLS transcoding, sprite
|
||||
# generation and audio extraction:
|
||||
#
|
||||
# - /tmp is NEVER written to. Not once, in any of those paths. The usual
|
||||
# "read-only rootfs, so mount an emptyDir on /tmp" is dead weight for
|
||||
# this image, and it is not mounted here.
|
||||
# - $HOME/.cache IS written to, and without it the log carries ten
|
||||
# "OSError: [Errno 30] Read-only file system: '/.cache'" lines. Both
|
||||
# bundled tools use it: deno for its module cache, yt-dlp for the
|
||||
# JavaScript challenge-solver library it fetches from github.com.
|
||||
#
|
||||
# The failure is a WARNING, not an error - the run continues. What it costs
|
||||
# is silent: the challenge-solver library is re-downloaded from github.com on
|
||||
# every single extraction instead of once, which turns an outage there, or an
|
||||
# egress rule that does not expect it, into broken playback with a warning
|
||||
# nobody read. With the volume mounted, both tools' caches land in /cache and
|
||||
# the log is clean.
|
||||
#
|
||||
# Nothing here is worth keeping across a restart - it is a cache in the
|
||||
# literal sense, refilled from the network.
|
||||
cacheSizeLimit: null
|
||||
|
||||
ingress:
|
||||
# Gates this chart's own Ingress. Everything the Ingress needs is read only
|
||||
# inside its template - the application itself needs no hostname of its own
|
||||
# (no base-URL setting, no absolute links, no url_for(_external=True)), so
|
||||
# switching the Ingress off frees BOTH ingress.domain and
|
||||
# ingress.clusterIssuer instead of only the issuer.
|
||||
#
|
||||
# WHO OWNS THE HOST, WITH THE PROXY ENABLED: the proxy sits in the data path,
|
||||
# so with oauth2-proxy.enabled=true the Ingress belongs to the PROXY
|
||||
# (oauth2-proxy.ingress) and this one is switched off; the app is then
|
||||
# reached only through its Service. Leaving both on for the same host does
|
||||
# not produce a merged route - ingress-nginx's admission webhook rejects the
|
||||
# second Ingress outright - so the chart refuses to render that combination.
|
||||
# Leaving this one on for a DIFFERENT host is allowed and means exactly what
|
||||
# it says: an unauthenticated way past the proxy.
|
||||
enabled: true
|
||||
clusterIssuer: null
|
||||
domain: null
|
||||
className: nginx
|
||||
# Extra annotations for the Ingress; merged with the cert-manager
|
||||
# cluster-issuer annotation the chart always sets.
|
||||
annotations: {}
|
||||
|
||||
# --- oauth2-proxy subchart -------------------------------------------------
|
||||
#
|
||||
# Everything below this line is passed to the oauth2-proxy chart
|
||||
# (https://oauth2-proxy.github.io/manifests), not read by this chart - with the
|
||||
# single exception of oauth2-proxy.enabled and oauth2-proxy.ingress.hosts,
|
||||
# which the two guards above consult. The keys are the subchart's, the full
|
||||
# list of them is in its own values.yaml, and this chart does not restrict
|
||||
# them: whatever it does not set here stays at the subchart's default.
|
||||
#
|
||||
# The subchart's own conventions are NOT this repo's. It is upstream software
|
||||
# and treated as such - we do not patch its templates, do not re-harden its
|
||||
# workload and do not re-implement its values. Two consequences worth knowing
|
||||
# before looking for them:
|
||||
#
|
||||
# - Its resource limits, probes and security context follow ITS defaults, not
|
||||
# the rules in README "Chart style guidelines". Set them under
|
||||
# oauth2-proxy.resources / .securityContext / .livenessProbe if you want
|
||||
# this repo's shape; the chart does not impose it on you.
|
||||
# - The 1Password operator's auto-restart and this repo's checksum/config
|
||||
# mechanism do not reach into it either. Repointing
|
||||
# secrets.oauth2ProxySecretRef at a different item changes the
|
||||
# OnePasswordItem this chart renders, but nothing in the subchart's pod
|
||||
# template - restart it by hand:
|
||||
# kubectl -n <ns> rollout restart deployment/<release>-oauth2-proxy
|
||||
#
|
||||
# The subchart itself has a conditional dependency on redis-ha (off by
|
||||
# default), which is why a helm dependency update here pulls two repositories.
|
||||
oauth2-proxy:
|
||||
# Off by default. The chart is fully usable without it, nothing of the
|
||||
# subchart is rendered, and no oauth2-proxy image is pulled.
|
||||
#
|
||||
# READ THIS BEFORE LEAVING IT OFF: the application has NO authentication of
|
||||
# any kind and this chart adds none. An Ingress without the proxy is an open
|
||||
# instance - anyone who reaches the URL can make it fetch and transcode
|
||||
# arbitrary videos from the internet, on your egress and your CPU.
|
||||
enabled: false
|
||||
|
||||
# Whether a new proxy version reaches the cluster is decided by the
|
||||
# dependency version in Chart.yaml, not here - see the comment at that line.
|
||||
# It is the only remaining path for a proxy update, because the subchart is
|
||||
# invisible to everything that watches Helm release versions.
|
||||
|
||||
ingress:
|
||||
# With the proxy enabled this is the Ingress that should be on, and
|
||||
# ingress.enabled above the one that should be off. Set hosts to the same
|
||||
# single host the app would otherwise carry.
|
||||
enabled: false
|
||||
className: nginx
|
||||
# hosts:
|
||||
# - player.example.com
|
||||
# tls:
|
||||
# - secretName: tls-player-example-com
|
||||
# hosts:
|
||||
# - player.example.com
|
||||
annotations: {}
|
||||
# cert-manager.io/cluster-issuer: letsencrypt
|
||||
# # The second half of the 4kB cookie trap below: the response carrying
|
||||
# # the session cookie does not fit in ingress-nginx's default 4k proxy
|
||||
# # buffer, and what the browser gets is a 502 AFTER a successful login.
|
||||
# nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
|
||||
|
||||
config:
|
||||
# The Secret holding client-id, client-secret and cookie-secret. This chart
|
||||
# renders it as an OnePasswordItem (see secrets.oauth2ProxySecretRef above)
|
||||
# exactly while this value still names it; point it at a Secret of your own
|
||||
# and the chart creates none.
|
||||
#
|
||||
# The subchart runs this string through tpl, so .Release.Name resolves.
|
||||
# .Chart.Name would NOT: inside the subchart it is "oauth2-proxy", so the
|
||||
# parent chart name is written out literally here.
|
||||
existingSecret: '{{ .Release.Name }}-ytdlp-web-player-oauth2-proxy-secret'
|
||||
|
||||
# The proxy's whole configuration, and the reason it is a configFile rather
|
||||
# than the structured config.upstreams: the subchart emits config.upstreams
|
||||
# with toJson and WITHOUT tpl, so a {{ .Release.Name }} written there ends
|
||||
# up in the manifest verbatim as those eleven characters. config.configFile
|
||||
# is passed through "tpl ... $", which is what makes the release name
|
||||
# reachable at all. A wrong upstream here is not a render error - it is a
|
||||
# 404 after a successful login, in production.
|
||||
configFile: |
|
||||
email_domains = ["*"]
|
||||
upstreams = ["http://{{ .Release.Name }}-ytdlp-web-player-service:80"]
|
||||
|
||||
# Trust X-Forwarded-*. Without it the proxy considers every request
|
||||
# plain http, and builds its redirect back to http:// - behind a
|
||||
# TLS-terminating Ingress that is a redirect loop the moment a login
|
||||
# completes.
|
||||
reverse_proxy = true
|
||||
|
||||
# Keep id_token and access_token OUT of the session cookie. With them in,
|
||||
# a user with a longer group list produces a cookie past the browsers'
|
||||
# 4kB limit; the browser drops it and the request comes back as HTTP 502
|
||||
# AFTER a successful login - with "[AuthSuccess]" in the proxy's log,
|
||||
# which is why this one is hunted in the wrong place for hours.
|
||||
#
|
||||
# It is incompatible with pass_access_token, set_authorization_header and
|
||||
# pass_authorization_header: switch any of those on together with this
|
||||
# and the proxy refuses to start at all. This app needs none of them - it
|
||||
# reads no identity headers whatsoever.
|
||||
session_cookie_minimal = true
|
||||
|
||||
# Set these to your provider. Example for an OIDC provider:
|
||||
# provider = "oidc"
|
||||
# oidc_issuer_url = "https://sso.example.com/application/o/player/"
|
||||
# redirect_url = "https://player.example.com/oauth2/callback"
|
||||
|
||||
# Leave this off. With alphaConfig.enabled=true the subchart generates a
|
||||
# configuration WITHOUT any upstreams and ignores config.configFile
|
||||
# silently - the login then succeeds and the app answers 404, because the
|
||||
# proxy has nothing to forward to. It is off in the subchart's defaults too;
|
||||
# it is spelled out here because switching it on looks like an upgrade.
|
||||
alphaConfig:
|
||||
enabled: false
|
||||
Reference in new issue
Block a user