Deploys matszwe02/ytdlp_web_player, a Flask app that fetches videos with
yt-dlp, transcodes them with ffmpeg and serves them as HLS. It ships no
authentication of any kind, so the chart carries a preconfigured oauth2-proxy
as an optional subchart, in the data path in front of the app.
This is the first chart in the repo with a dependency, so the rule "charts
have no dependencies" falls here. It is replaced in CLAUDE.md and README.md by
a bar: a dependency has to be optional and off by default, upstream's chart is
consumed as it comes (documented where it falls short of our conventions,
never patched), and the price is written down - above all that a subchart is
invisible to everything downstream watching Helm release versions, so a new
proxy release only reaches a cluster when this chart is republished.
CI learns the dependency in five places, not the three the plan expected:
helm lint, package, template and install all exit 1 while charts/ is unfilled,
so ci/helm-dependencies.sh runs before lint and package in validate-charts.yml,
before package in publish-helm-chart.yml, and inside run-install-test.sh,
run-trivy-config.sh and run-quantity-render-test.sh - the last of which renders
every chart on every PR and would have gone red first.
Notable chart decisions, each measured rather than assumed:
- The image has no USER and runs as root by default, but not by necessity: it
runs under an arbitrary uid with readOnlyRootFilesystem and the full
hardening baseline. Only the pinned uid is a deviation, and it is documented.
- Two emptyDirs, each justified separately. /data holds the video cache
(DATA_PATH). /tmp is NOT mounted - measured across metadata extraction, HLS
transcoding, sprite generation and audio extraction, the image never writes
there. What it does write is $HOME/.cache, so /cache is mounted with HOME and
XDG_CACHE_HOME pointed at it; without it yt-dlp re-downloads its JavaScript
challenge-solver from github.com on every extraction.
- The proxy is wired through config.configFile, the only value the subchart
passes through tpl - config.upstreams is emitted with toJson and would carry
a literal {{ .Release.Name }} into the manifest.
- Rendering aborts when this chart's Ingress and the proxy's claim the same
host: ingress-nginx rejects the second one and a Fleet bundle then sits in
ErrApplied until forceSyncGeneration is bumped.
Fixes #214
136 lines
6.6 KiB
YAML
136 lines
6.6 KiB
YAML
{{- 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
|