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
|
- rss-bridge
|
||||||
- searxng
|
- searxng
|
||||||
- bentopdf
|
- bentopdf
|
||||||
|
- ytdlp-web-player
|
||||||
# freesailarr is deliberately absent: none of its nine images takes
|
# freesailarr is deliberately absent: none of its nine images takes
|
||||||
# its tag from appVersion (they are all pinned in its values.yaml),
|
# its tag from appVersion (they are all pinned in its values.yaml),
|
||||||
# so this workflow would rewrite appVersion and republish the chart
|
# so this workflow would rewrite appVersion and republish the chart
|
||||||
|
|||||||
@@ -43,6 +43,17 @@ jobs:
|
|||||||
uses: 1password/load-secrets-action/configure@v5
|
uses: 1password/load-secrets-action/configure@v5
|
||||||
with:
|
with:
|
||||||
connect-host: https://op-connect.jk-dev.de
|
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"
|
- name: "Package Helm Chart"
|
||||||
run: helm package ${{ inputs.chart-name }}
|
run: helm package ${{ inputs.chart-name }}
|
||||||
working-directory: ${{ inputs.chart-location }}
|
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
|
fetch-depth: 0
|
||||||
- name: "Install Helm"
|
- name: "Install Helm"
|
||||||
uses: azure/setup-helm@v5
|
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"
|
- name: "Lint Helm Chart"
|
||||||
run: helm lint --strict "${{ matrix.chart }}"
|
run: helm lint --strict "${{ matrix.chart }}"
|
||||||
- name: "Package Helm Chart"
|
- name: "Package Helm Chart"
|
||||||
|
|||||||
+23
@@ -1,6 +1,29 @@
|
|||||||
### Example user template template
|
### Example user template template
|
||||||
### Example user 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
|
# IntelliJ project files
|
||||||
.idea
|
.idea
|
||||||
*.iml
|
*.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.
|
- 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).
|
- 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.
|
- 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 |
|
| `template-chart` | — | Scaffold for new charts (placeholder `xxx`), never published |
|
||||||
| `urlaubsverwaltung` | Urlaubsverwaltung | Deployment, DB + OIDC secrets |
|
| `urlaubsverwaltung` | Urlaubsverwaltung | Deployment, DB + OIDC secrets |
|
||||||
| `voucher-vault` | VoucherVault | StatefulSet, 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 |
|
| `zipline` | Zipline | StatefulSet, DB secret |
|
||||||
|
|
||||||
## Versioning
|
## 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-<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.
|
- `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.
|
- `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.
|
- `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)
|
### 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.
|
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:
|
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.
|
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`).
|
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`.
|
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:
|
Validate locally with:
|
||||||
|
|
||||||
@@ -201,7 +204,7 @@ These guidelines are **binding for every current and future chart** in this repo
|
|||||||
### 7. Resources (auto-limits)
|
### 7. Resources (auto-limits)
|
||||||
|
|
||||||
- Resource requests ship as chart defaults in `values.yaml` and are user-overridable.
|
- 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
|
### 8. Secrets
|
||||||
|
|
||||||
@@ -277,4 +280,23 @@ Reference implementation: `template-chart/values.schema.json` and `template-char
|
|||||||
- `helm lint --strict` must pass without findings.
|
- `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.
|
- **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.
|
- 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::"
|
echo "::endgroup::"
|
||||||
|
|
||||||
# --- 3. Install the chart ---------------------------------------------------
|
# --- 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"
|
echo "::group::helm install"
|
||||||
helm install "$RELEASE" "$ROOT/$CHART" \
|
helm install "$RELEASE" "$ROOT/$CHART" \
|
||||||
--namespace "$NAMESPACE" \
|
--namespace "$NAMESPACE" \
|
||||||
|
|||||||
@@ -26,9 +26,9 @@
|
|||||||
# case is not - it needs the rendered text to be searched for a quantity in
|
# case is not - it needs the rendered text to be searched for a quantity in
|
||||||
# exponential notation. Both are checked here, per chart.
|
# exponential notation. Both are checked here, per chart.
|
||||||
#
|
#
|
||||||
# Why over *all* charts instead of the changed ones: charts in this repo have
|
# Why over *all* charts instead of the changed ones: the helper is copied into
|
||||||
# no dependencies, so the helper is copied into every chart's
|
# every chart's templates/_helpers.tpl rather than shared through a library
|
||||||
# templates/_helpers.tpl. The regression this guards against arrives by copy -
|
# chart. The regression this guards against arrives by copy -
|
||||||
# a new chart started from an older chart, or from template-chart before the
|
# 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
|
# 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
|
# 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
|
echo "ERROR: '$CHART' is not a chart directory" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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"
|
SCHEMA="$ROOT/$CHART/values.schema.json"
|
||||||
if [ ! -f "$SCHEMA" ]; then
|
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."
|
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
|
PY
|
||||||
|
|
||||||
# --- 3. Render ---------------------------------------------------------------
|
# --- 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)"
|
echo "::group::helm template $CHART ($VALUES_LABEL)"
|
||||||
# ${a[@]+"${a[@]}"} - an empty array under `set -u` is an error on bash 3.2
|
# ${a[@]+"${a[@]}"} - an empty array under `set -u` is an error on bash 3.2
|
||||||
# (the macOS default), where this script also runs locally.
|
# (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"],
|
matchDepTypes: ["app-version"],
|
||||||
addLabels: ["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: [
|
description: [
|
||||||
"nginx numbers its release branches by minor: even is the stable branch, odd is",
|
"nginx numbers its release branches by minor: even is the stable branch, odd is",
|
||||||
@@ -396,6 +417,32 @@
|
|||||||
datasourceTemplate: "docker",
|
datasourceTemplate: "docker",
|
||||||
depTypeTemplate: "app-version",
|
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",
|
customType: "regex",
|
||||||
managerFilePatterns: ["zipline/Chart.yaml"],
|
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