Актуализация чарта по продовому .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>
233 lines
11 KiB
YAML
233 lines
11 KiB
YAML
{{- $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 }}
|