DEVOPS-117: чарт on-premise 3.0 — Nanabush Player, секреты в Secret, валидация values

Актуализация чарта по продовому .helm из HRBOX.

Nanabush Player перенесён из прода и упрощён под one-release-топологию
on-premise: без карт env/ci_dc, но с сохранёнными инвариантами (audience =
https://<host>, basePath отдельно от aud, allowlist обратного канала только
на внутрикластерный web, метрики на непубликуемом порту). Выделенного
поддомена в on-premise нет, поэтому плеер монтируется на основной домен под
/nanabush-player. Три переключателя для аварийного отката сохранены.

Из прода также перенесены: пул конвертации видео, PodDisruptionBudget для
web, OpenTelemetry-сайдкар, assetlinks, набор location в nginx (/healthz,
/health-check, потоковый /api/v1/integration/commit, /ai/, sw.js, шрифты).
Версии образов подтянуты к продовым.

Приведение чарта в порядок: все образы в блоке image, imagePullSecrets в
настройках, девять одинаковых define ресурсов заменены одним хелпером,
удалено ~100 строк мёртвого кода в _helpers.tpl, пароли и ключи уехали в
Secret и читаются через secretKeyRef, добавлены 00-validate.yaml и NOTES.txt.

Исправлено в 2.x:
- SERVICES_INTERNAL_SECRET имел захардкоженное значение по умолчанию, то есть
  все установки, где его не заполнили, работали на одном секрете межсервисной
  аутентификации;
- cluster.domain игнорировался: маршруты кластера NATS были захардкожены на
  cluster.local;
- CLUSTER_ADVERTISE без сегмента svc — ноды NATS объявляли соседям
  недостижимый адрес;
- nats.replicas и nats.cluster.replicas задавались независимо, при
  расхождении часть нод не входила в кластер;
- пробы web проверяли только TCP-порт 9000, поэтому под с неработающим PHP
  считался готовым;
- внутренние URL были захардкожены строками hrbox-* и молча ломались при
  nameOverride.

Имена объектов и селекторы Deployment не менялись — обновление 2.x -> 3.0 не
пересоздаёт объекты. Переехавшие ключи values.yaml чарт распознаёт и
останавливает установку с подсказкой; порядок перехода — в CHANGELOG.md.

Проверено: helm lint чистый; отрендерены 4 конфигурации (дефолт, всё
включено, плеер и инфра выключены, nameOverride); структурная проверка
рендера без замечаний; 20 негативных сценариев срабатывают с ожидаемыми
сообщениями; реалистичный values.yaml из 2.x последовательно ловится всеми
шестью guard'ами.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 16:37:42 +06:00
co-authored by Claude Opus 5
parent 6363382f09
commit efc506e5de
35 changed files with 3150 additions and 1118 deletions
+232
View File
@@ -0,0 +1,232 @@
{{- $cfg := .Values.app.nanabushPlayer.deployment }}
{{- if $cfg.enabled }}
{{- $name := include "nanabushPlayer.serviceName" . -}}
{{- $secretName := include "nanabushPlayer.secretName" . -}}
{{/*
Nanabush Player — проигрыватель учебных материалов: SCORM 1.2 и 2004, cmi5,
xAPI, нативные курсы, тесты и эссе.
Как это работает:
* HRBox выпускает launch-тикет, подписанный общим ключом ticketHs256,
и отправляет браузер на https://<домен>{{ include "nanabushPlayer.basePath" . }}/start;
* плеер проверяет подпись, audience и issuer, заводит свою сессию;
* за пакетом курса и для отправки отчётов плеер ходит обратно в HRBox по
внутрикластерному адресу — только он и разрешён в allowlist;
* учебные события копятся в Redis Streams и досылаются форвардером,
поэтому короткая недоступность HRBox не теряет прогресс.
Ограничения on-premise описаны в values.yaml рядом с app.nanabushPlayer.
Полный контракт — docs/nanabush-player-deployment.md в репозитории HRBox.
*/}}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $name }}
labels:
{{- include "hrbox.labels" (dict "ctx" . "name" "nanabush-player") | nindent 4 }}
spec:
replicas: {{ $cfg.replicas }}
revisionHistoryLimit: 3
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
service: {{ $name }}
template:
metadata:
labels:
service: {{ $name }}
app: nanabush-player
{{- if $cfg.metrics.enabled }}
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: {{ $cfg.metrics.port | quote }}
prometheus.io/path: "/metrics"
{{- end }}
spec:
# Плееру не нужен доступ к API Kubernetes, а исполняет он содержимое,
# загруженное пользователями, — поэтому токен не монтируется, процесс
# не root, корневая файловая система только на чтение.
automountServiceAccountToken: false
terminationGracePeriodSeconds: {{ $cfg.terminationGracePeriodSeconds }}
{{- include "hrbox.imagePullSecrets" . | nindent 6 }}
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: player
image: {{ .Values.image.nanabushPlayer | quote }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
ports:
- name: http
containerPort: {{ .Values.app.nanabushPlayer.service.port }}
protocol: TCP
{{- if $cfg.metrics.enabled }}
- name: metrics
containerPort: {{ $cfg.metrics.port }}
protocol: TCP
{{- end }}
env:
- name: PLAYER_HTTP_ADDR
value: {{ printf ":%d" (.Values.app.nanabushPlayer.service.port | int) | quote }}
- name: PLAYER_METRICS_ADDR
{{- if $cfg.metrics.enabled }}
value: {{ printf ":%d" ($cfg.metrics.port | int) | quote }}
{{- else }}
value: "off"
{{- end }}
# Путь монтирования не входит в audience: audience — это origin
# браузера, иначе сломались бы проверки на стороне клиента.
- name: PLAYER_BASE_PATH
value: {{ include "nanabushPlayer.basePath" . | quote }}
- name: PLAYER_AUDIENCE
value: {{ include "nanabushPlayer.publicOrigin" . | quote }}
- name: PLAYER_ALLOWED_ISSUERS
value: {{ include "nanabushPlayer.issuer" . | quote }}
- name: PLAYER_HS256_SECRET
valueFrom:
secretKeyRef:
name: {{ $secretName | quote }}
key: {{ required "Нужно имя ключа ticketHs256 в секрете Nanabush Player" $cfg.existingSecret.keys.ticketHs256 | quote }}
{{- with $cfg.existingSecret.keys.ticketKeys }}
# Кольцо ключей: подпись проверяется ключом, выбранным по kid из
# тикета, поэтому во время ротации работают обе версии HRBox.
- name: PLAYER_TICKET_KEYS
valueFrom:
secretKeyRef:
name: {{ $secretName | quote }}
key: {{ . | quote }}
{{- with $cfg.ticket.keylessAcceptedUntil }}
# Только на первую раскатку: принимать тикеты без kid от версии
# HRBox, которая ещё не знает про кольцо ключей.
- name: PLAYER_TICKET_KEYLESS_UNTIL
value: {{ . | quote }}
{{- end }}
{{- end }}
- name: PLAYER_SESSION_SECRET
valueFrom:
secretKeyRef:
name: {{ $secretName | quote }}
key: {{ required "Нужно имя ключа sessionHs256 в секрете Nanabush Player" $cfg.existingSecret.keys.sessionHs256 | quote }}
- name: PLAYER_SESSION_ISSUER
value: {{ include "nanabushPlayer.publicOrigin" . | quote }}
- name: PLAYER_SESSION_TTL
value: {{ $cfg.ticket.sessionTtl | quote }}
- name: PLAYER_MAX_TICKET_LIFETIME
value: {{ $cfg.ticket.maxLifetime | quote }}
- name: REDIS_URL
{{- if $cfg.redis.urlFromExistingSecret }}
valueFrom:
secretKeyRef:
name: {{ $secretName | quote }}
key: {{ required "Нужно имя ключа redisUrl, когда redis.urlFromExistingSecret: true" $cfg.existingSecret.keys.redisUrl | quote }}
{{- else }}
value: {{ include "nanabushPlayer.redisUrl" . | quote }}
{{- end }}
- name: PLAYER_REDIS_KEY_PREFIX
value: {{ include "nanabushPlayer.redisKeyPrefix" . | quote }}
# Обратный канал: единственный разрешённый адрес — внутрикластерный
# HRBox. Так пакет курса не сможет заставить плеер сходить наружу.
- name: PLAYER_PACKAGE_FETCH_SCHEMES
value: {{ $cfg.backchannel.schemes | quote }}
- name: PLAYER_PACKAGE_FETCH_ALLOWLIST
value: {{ include "nanabushPlayer.backchannelHost" . | quote }}
- name: PLAYER_PACKAGE_FETCH_ALLOW_PRIVATE_IPS
value: {{ $cfg.backchannel.allowPrivateIps | quote }}
- name: PLAYER_PACKAGE_FETCH_MAX_BYTES
value: {{ $cfg.backchannel.packageFetchMaxBytes | quote }}
- name: PLAYER_PACKAGE_CONTENT_MAX_BYTES
value: {{ $cfg.backchannel.packageContentMaxBytes | quote }}
- name: PLAYER_PACKAGE_FETCH_TIMEOUT
value: {{ $cfg.backchannel.packageFetchTimeout | quote }}
- name: PLAYER_PACKAGE_STREAM_TIMEOUT
value: {{ $cfg.backchannel.packageStreamTimeout | quote }}
- name: PLAYER_REPORT_ENDPOINT_SCHEMES
value: {{ $cfg.backchannel.schemes | quote }}
- name: PLAYER_REPORT_ENDPOINT_ALLOWLIST
value: {{ include "nanabushPlayer.backchannelHost" . | quote }}
- name: PLAYER_REPORT_ENDPOINT_ALLOW_PRIVATE_IPS
value: {{ $cfg.backchannel.allowPrivateIps | quote }}
- name: PLAYER_REPORT_ENDPOINT_TIMEOUT
value: {{ $cfg.backchannel.reportTimeout | quote }}
- name: PLAYER_FORWARDER_QUEUE_SIZE
value: {{ $cfg.forwarder.queueSize | quote }}
- name: PLAYER_FORWARDER_WORKERS
value: {{ $cfg.forwarder.workers | quote }}
- name: PLAYER_FORWARDER_MAX_ATTEMPTS
value: {{ $cfg.forwarder.maxAttempts | quote }}
- name: PLAYER_FORWARDER_INITIAL_BACKOFF
value: {{ $cfg.forwarder.initialBackoff | quote }}
- name: PLAYER_FORWARDER_MAX_BACKOFF
value: {{ $cfg.forwarder.maxBackoff | quote }}
- name: PLAYER_FORWARDER_PER_ATTEMPT_TIMEOUT
value: {{ $cfg.forwarder.perAttemptTimeout | quote }}
- name: PLAYER_FORWARDER_DEAD_LETTER_SIZE
value: {{ $cfg.forwarder.deadLetterSize | quote }}
- name: PLAYER_FORWARDER_CLAIM_IDLE
value: {{ $cfg.forwarder.claimIdle | quote }}
- name: PLAYER_FORWARDER_POLL_INTERVAL
value: {{ $cfg.forwarder.pollInterval | quote }}
- name: PLAYER_FORWARDER_RATE_LIMIT_EVENTS_PER_MINUTE
value: {{ $cfg.forwarder.rateLimitEventsPerMinute | quote }}
- name: PLAYER_FORWARDER_RATE_LIMIT_BATCHES_PER_MINUTE
value: {{ $cfg.forwarder.rateLimitBatchesPerMinute | quote }}
- name: PLAYER_FORWARDER_RATE_LIMIT_PAYLOAD_BYTES_PER_MINUTE
value: {{ $cfg.forwarder.rateLimitPayloadBytesPerMinute | quote }}
- name: LOG_LEVEL
value: {{ $cfg.log.level | quote }}
- name: LOG_FORMAT
value: {{ $cfg.log.format | quote }}
- name: PLAYER_DEV_HMR
value: "false"
readinessProbe:
httpGet:
path: {{ $cfg.probes.readiness.path }}
port: http
initialDelaySeconds: {{ $cfg.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ $cfg.probes.readiness.periodSeconds }}
timeoutSeconds: {{ $cfg.probes.readiness.timeoutSeconds }}
failureThreshold: {{ $cfg.probes.readiness.failureThreshold }}
livenessProbe:
httpGet:
path: {{ $cfg.probes.liveness.path }}
port: http
initialDelaySeconds: {{ $cfg.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ $cfg.probes.liveness.periodSeconds }}
timeoutSeconds: {{ $cfg.probes.liveness.timeoutSeconds }}
failureThreshold: {{ $cfg.probes.liveness.failureThreshold }}
{{- include "hrbox.resources" (dict "ctx" . "component" "nanabushPlayer") | indent 10 }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $name }}
labels:
{{- include "hrbox.labels" (dict "ctx" . "name" "nanabush-player") | nindent 4 }}
spec:
type: ClusterIP
selector:
service: {{ $name }}
# Порт метрик наружу намеренно не публикуется: содержимое курсов приходит
# с того же origin и не должно читать эксплуатационные метрики.
ports:
- name: http
port: {{ .Values.app.nanabushPlayer.service.port }}
targetPort: http
protocol: TCP
{{- end }}