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
+211 -90
View File
@@ -1,110 +1,231 @@
{{/*
Expand the name of the chart.
============================================================================
Общие хелперы чарта HRBox
============================================================================
Правило именования: все объекты называются "<имя чарта>-<компонент>"
(hrbox-web, hrbox-worker, ...). Имя чарта можно переопределить через
nameOverride, но по умолчанию оно равно "hrbox" — так же, как в чарте 2.x,
поэтому обновление 2.x -> 3.x не пересоздаёт объекты.
*/}}
{{- define "nats.name" -}}
{{- default .Chart.Name .Values.nats.nameOverride | trunc 63 | trimSuffix "-" -}}
{{/* Базовое имя релиза: "hrbox" или nameOverride. */}}
{{- define "hrbox.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/* Имя чарта с версией — для лейбла helm.sh/chart. */}}
{{- define "hrbox.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "nats.fullname" -}}
{{- if .Values.nats.fullnameOverride -}}
{{- .Values.nats.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{/*
Полное имя объекта компонента.
Usage: {{ include "hrbox.component" (dict "ctx" . "name" "web") }} -> hrbox-web
*/}}
{{- define "hrbox.component" -}}
{{- printf "%s-%s" (include "hrbox.name" .ctx) .name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Селектор пода. ВАЖНО: spec.selector в Deployment неизменяем, поэтому набор
лейблов здесь совпадает с чартом 2.x (один лейбл "service") и меняться не должен.
Usage: {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "web") | nindent 6 }}
*/}}
{{- define "hrbox.selectorLabels" -}}
service: {{ include "hrbox.component" (dict "ctx" .ctx "name" .name) }}
{{- end -}}
{{/*
Лейблы объекта. Лейбл "app" сохранён из чарта 2.x — на него опираются
привычные команды вида "kubectl get pods -l app=worker".
Usage: {{- include "hrbox.labels" (dict "ctx" . "name" "web") | nindent 4 }}
*/}}
{{- define "hrbox.labels" -}}
app: {{ .name }}
app.kubernetes.io/name: {{ include "hrbox.name" .ctx }}
app.kubernetes.io/component: {{ .name }}
app.kubernetes.io/instance: {{ .ctx.Release.Name }}
app.kubernetes.io/managed-by: {{ .ctx.Release.Service }}
app.kubernetes.io/version: {{ .ctx.Chart.AppVersion | quote }}
helm.sh/chart: {{ include "hrbox.chart" .ctx }}
{{- end -}}
{{/*
Секреты для доступа к приватному Docker-реестру.
Usage: {{- include "hrbox.imagePullSecrets" . | nindent 6 }}
*/}}
{{- define "hrbox.imagePullSecrets" -}}
{{- if .Values.imagePullSecrets }}
imagePullSecrets:
{{- range .Values.imagePullSecrets }}
- name: {{ .name | quote }}
{{- end }}
{{- end }}
{{- end -}}
{{/*
Тег из ссылки на образ: "registry/repo:1.2.3" -> "1.2.3".
Двоеточие ищется только в последнем сегменте пути, чтобы не спутать тег
с портом реестра ("registry:5000/repo").
*/}}
{{- define "hrbox.imageTag" -}}
{{- $lastSegment := splitList "/" . | last -}}
{{- if contains ":" $lastSegment -}}
{{- splitList ":" $lastSegment | last -}}
{{- end -}}
{{- end -}}
{{/*
============================================================================
Секрет приложения
============================================================================
Чувствительные значения (пароли БД и SMTP, ключи шифрования, токены) не
попадают в env-блоки Deployment открытым текстом: чарт складывает их в один
Secret, а контейнеры читают его через secretKeyRef. Полный список ключей —
в README, раздел "Секреты".
secrets.existingSecret позволяет подставить Secret, созданный вне чарта
(например, из внешнего vault-оператора) — тогда чарт свой Secret не создаёт.
*/}}
{{/*
Адреса внутренних сервисов.
Сервисы релиза адресуются по именам, которые создаёт этот же чарт, поэтому
пустое значение в values резолвится в имя из hrbox.component: адреса не
разъезжаются при nameOverride и их не нужно править вручную. Непустое
значение используется как есть — для сервиса за пределами релиза.
*/}}
{{- define "hrbox.geonamesUrl" -}}
{{- .Values.app.geonames.url | default (printf "http://%s:8181" (include "hrbox.component" (dict "ctx" . "name" "geonames"))) -}}
{{- end -}}
{{- define "hrbox.goworkerUrl" -}}
{{- .Values.app.goworker.url | default (printf "http://%s:8686" (include "hrbox.component" (dict "ctx" . "name" "goworker"))) -}}
{{- end -}}
{{- define "hrbox.kedocaUrl" -}}
{{- .Values.app.kedoca.url | default (printf "http://%s:8558" (include "hrbox.component" (dict "ctx" . "name" "kedoca"))) -}}
{{- end -}}
{{- define "hrbox.redisHost" -}}
{{- .Values.app.redis.host | default (include "hrbox.component" (dict "ctx" . "name" "dragonfly")) -}}
{{- end -}}
{{- define "hrbox.natsUrl" -}}
{{- .Values.app.nats.url | default (printf "nats://%s:4222" (include "hrbox.component" (dict "ctx" . "name" "nats"))) -}}
{{- end -}}
{{- define "hrbox.secretName" -}}
{{- if .Values.secrets.existingSecret -}}
{{- .Values.secrets.existingSecret -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nats.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- include "hrbox.component" (dict "ctx" . "name" "secrets") -}}
{{- end -}}
{{- end -}}
{{/*
Одна переменная окружения из секрета приложения.
Usage: {{ include "hrbox.secretEnv" (dict "ctx" . "env" "POSTGRES_PASSWORD" "key" "postgres-password") }}
*/}}
{{- define "hrbox.secretEnv" -}}
- name: {{ .env }}
valueFrom:
secretKeyRef:
name: {{ include "hrbox.secretName" .ctx | quote }}
key: {{ .key | quote }}
{{- end -}}
{{/*
============================================================================
Nanabush Player
============================================================================
On-premise-топология Player'а отличается от облачной: выделенного поддомена
нет, поэтому Player живёт на основном домене HRBox под путём basePath
(по умолчанию /nanabush-player). Отсюда следуют три инварианта, которые
проверяет 00-validate.yaml:
* audience тикета = https://<ingress.host> (origin без пути)
* basePath = отдельная переменная, в aud не входит
* issuer тикета = https://<ingress.host>/lms/player
Подробности контракта — docs/nanabush-player-deployment.md в репозитории HRBox.
*/}}
{{- define "nanabushPlayer.host" -}}
{{- required "Для Nanabush Player нужен непустой ingress.host" .Values.ingress.host -}}
{{- end -}}
{{- define "nanabushPlayer.publicOrigin" -}}
{{- printf "https://%s" (include "nanabushPlayer.host" .) -}}
{{- end -}}
{{/*
Путь монтирования Player'а на основном домене. Пустое значение и "/" запрещены:
это отдало бы Player'у корень, занятый самим HRBox.
*/}}
{{- define "nanabushPlayer.basePath" -}}
{{- $path := .Values.app.nanabushPlayer.basePath | default "/nanabush-player" -}}
{{- $path | trimSuffix "/" -}}
{{- end -}}
{{- define "nanabushPlayer.issuer" -}}
{{- if .Values.app.nanabushPlayer.issuer -}}
{{- .Values.app.nanabushPlayer.issuer -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- printf "https://%s/lms/player" (include "nanabushPlayer.host" .) -}}
{{- end -}}
{{- end -}}
{{- define "nats.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "nanabushPlayer.serviceName" -}}
{{- .Values.app.nanabushPlayer.service.name | default (include "hrbox.component" (dict "ctx" . "name" "nanabush-player")) -}}
{{- end -}}
{{/*
Common labels
Хост обратного канала Player -> HRBox. Только внутрикластерное имя: по нему
Player забирает пакеты курсов и отправляет отчёты, и оно же попадает в
allowlist самого Player'а.
*/}}
{{- define "nats.labels" -}}
app.kubernetes.io/name: nats
app.kubernetes.io/instance: {{ .Release.Name }}
helm.sh/chart: {{ include "nats.chart" . }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "nats.selectorLabels" -}}
app.kubernetes.io/name: nats
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Return the proper NATS image name
*/}}
{{- define "nats.clusterAdvertise" -}}
{{- printf "$(POD_NAME).%s.$(POD_NAMESPACE).svc.%s" (include "nats.fullname" . ) $.Values.nats.k8sClusterDomain }}
{{- end }}
{{/*
Return the NATS cluster routes.
*/}}
{{- define "nats.clusterRoutes" -}}
{{- $name := (include "nats.fullname" . ) -}}
{{- range $i, $e := until (.Values.nats.cluster.replicas | int) -}}
{{- printf "nats://%s-%d.%s.%s.svc.%s:6222," $name $i $name $.Release.Namespace $.Values.nats.k8sClusterDomain -}}
{{- define "nanabushPlayer.backchannelHost" -}}
{{- .Values.app.nanabushPlayer.deployment.backchannel.host | default (include "hrbox.component" (dict "ctx" . "name" "web")) -}}
{{- end -}}
{{- end }}
{{- define "nats.extraRoutes" -}}
{{- range $i, $url := .Values.nats.cluster.extraRoutes -}}
{{- printf "%s," $url -}}
{{- define "nanabushPlayer.internalOrigin" -}}
{{- printf "http://%s" (include "nanabushPlayer.backchannelHost" .) -}}
{{- end -}}
{{- end }}
{{- define "nats.tlsConfig" -}}
tls {
{{- if .cert }}
cert_file: {{ .secretPath }}/{{ .secret.name }}/{{ .cert }}
{{- end }}
{{- if .key }}
key_file: {{ .secretPath }}/{{ .secret.name }}/{{ .key }}
{{- end }}
{{- if .ca }}
ca_file: {{ .secretPath }}/{{ .secret.name }}/{{ .ca }}
{{- end }}
{{- if .insecure }}
insecure: {{ .insecure }}
{{- end }}
{{- if .verify }}
verify: {{ .verify }}
{{- end }}
{{- if .verifyAndMap }}
verify_and_map: {{ .verifyAndMap }}
{{- end }}
{{- if .curvePreferences }}
curve_preferences: {{ .curvePreferences }}
{{- end }}
{{- if .timeout }}
timeout: {{ .timeout }}
{{- end }}
}
{{- end }}
{{/*
Renders a value that contains template.
Usage:
{{ include "tplvalues.render" ( dict "value" .Values.nats.path.to.the.Value "context" $) }}
Версия Player'а берётся из тега образа, а не из отдельной настройки: тикет
не должен объявлять версию, отличную от той, что реально запущена в кластере.
*/}}
{{- define "tplvalues.render" -}}
{{- if typeIs "string" .value }}
{{- tpl .value .context }}
{{- else }}
{{- tpl (.value | toYaml) .context }}
{{- end }}
{{- define "nanabushPlayer.version" -}}
{{- $tag := include "hrbox.imageTag" .Values.image.nanabushPlayer -}}
{{- if not $tag -}}
{{- fail "image.nanabushPlayer должен содержать явный тег версии, например nanabush-player:1.0.11" -}}
{{- end -}}
{{- $tag -}}
{{- end -}}
{{- define "nanabushPlayer.secretName" -}}
{{- required "app.nanabushPlayer.deployment.existingSecret.name обязателен, когда Player включён" .Values.app.nanabushPlayer.deployment.existingSecret.name -}}
{{- end -}}
{{/* URL Redis, который чарт собирает из штатных настроек app.redis. */}}
{{- define "nanabushPlayer.redisUrl" -}}
{{- printf "redis://%s:%s/%d" (include "hrbox.redisHost" .) (.Values.app.redis.port | toString) (.Values.app.nanabushPlayer.deployment.redis.database | int) -}}
{{- end -}}
{{/*
Префикс ключей Redis. Namespace в префиксе разводит несколько релизов HRBox,
живущих на одном Dragonfly.
*/}}
{{- define "nanabushPlayer.redisKeyPrefix" -}}
{{- printf "hrbox:%s:%s" .Release.Namespace .Values.app.nanabushPlayer.deployment.redis.keyPrefixSuffix -}}
{{- end -}}