diff --git a/aero/pyproject.toml b/aero/pyproject.toml index ef3a360..1c2cb1d 100644 --- a/aero/pyproject.toml +++ b/aero/pyproject.toml @@ -44,7 +44,8 @@ platform = "ansible-playbook deploy.yml -e aero_platform_up=true" gen-env = "ansible-playbook deploy.yml --tags env" # docker login в приватный реестр по ключу json_key login = "ansible-playbook deploy.yml --tags login" -# Сгенерировать логин+пароль и создать Django-суперпользователя в backend +# Показать логин+пароль администратора платформы. Саму учётку создаёт под +# backend при старте (apps/django/aero), эта задача только печатает креды. superuser = "ansible-playbook deploy.yml -e sarex_create_superuser=true --tags superuser" lint = "ansible-lint" diff --git a/aero/roles/sarex_stack/defaults/main.yml b/aero/roles/sarex_stack/defaults/main.yml index e86c6c4..2b3010e 100644 --- a/aero/roles/sarex_stack/defaults/main.yml +++ b/aero/roles/sarex_stack/defaults/main.yml @@ -263,6 +263,15 @@ vault_app_secrets: secret_key: "{{ sarex_secrets.SAREX_MINIO_ROOT_PASSWORD }}" buckets: - name: "{{ sarex_django_s3_bucket }}" + # Учётка администратора платформы. Её разбирает не приложение, а стартовый + # скрипт пода backend: он экспортирует ключи как DJANGO_SUPERUSER_* — ровно + # те имена, которые ждёт штатная manage.py createsuperuser --noinput. + # См. apps/django/aero/kustomization.yaml. + - path: secrets/apps/django/superuser + data: + username: "{{ superuser_name }}" + password: "{{ superuser_password }}" + email: "{{ superuser_name }}@{{ platform_domain }}" # JWT-ключи RS512. Те же самые, что роль генерирует для compose-стека, — # контур переезжает, и токены должны остаться совместимыми. - path: secrets/vault/common/rsa_keys diff --git a/aero/roles/sarex_stack/tasks/main.yml b/aero/roles/sarex_stack/tasks/main.yml index 8d39f89..2772494 100644 --- a/aero/roles/sarex_stack/tasks/main.yml +++ b/aero/roles/sarex_stack/tasks/main.yml @@ -92,6 +92,28 @@ no_log: true tags: [env] +# Учётка администратора платформы. Живёт отдельно от общего цикла выше по двум +# причинам: у логина свой алфавит (Django не примет username со спецсимволами) +# и оба значения не должны попасть в .env — compose-стек их не использует, +# потребитель один, и это Vault. +# +# Генерируется всегда, а не по флагу sarex_create_superuser: значения уезжают +# в secrets/apps/django/superuser (см. vault_app_secrets), откуда их забирает +# стартовый скрипт пода backend. Без них vault-agent не отрендерит шаблон и +# под встанет в Init:Error. lookup('password') идемпотентен — на повторных +# прогонах возвращает уже сохранённое значение, а не генерирует новое. +- name: Сгенерировать/загрузить логин администратора платформы + ansible.builtin.set_fact: + superuser_name: "{{ lookup('password', secrets_store ~ '/' ~ inventory_hostname ~ '/SAREX_SUPERUSER_NAME chars=ascii_lowercase,digits length=10') }}" + no_log: true + tags: [env, superuser] + +- name: Сгенерировать/загрузить пароль администратора платформы + ansible.builtin.set_fact: + superuser_password: "{{ lookup('password', secrets_store ~ '/' ~ inventory_hostname ~ '/SAREX_SUPERUSER_PASSWORD chars=ascii_letters,digits length=' ~ secret_length) }}" + no_log: true + tags: [env, superuser] + # RSA-ключи JWT (RS512): генерируем на control-node, persist в secrets_store. - name: Сгенерировать приватный RSA-ключ JWT ansible.builtin.command: "openssl genrsa -out {{ jwt_private_key_path }} 4096" @@ -181,89 +203,16 @@ changed_when: true tags: [up] -# --- Django-суперпользователь: генерация логина+пароля, создание ----------- -# Только по явной команде (uv run poe superuser передаёт sarex_create_superuser=true), -# чтобы обычный deploy не дёргал backend. Логин/пароль персистятся в secrets_store -# (переиспользуются при повторе — идемпотентно). -- name: Сгенерировать логин суперпользователя - ansible.builtin.set_fact: - superuser_name: "{{ lookup('password', secrets_store ~ '/' ~ inventory_hostname ~ '/SAREX_SUPERUSER_NAME chars=ascii_lowercase,digits length=10') }}" - when: sarex_create_superuser | default(false) | bool - no_log: true - tags: [superuser] - -- name: Сгенерировать пароль суперпользователя - ansible.builtin.set_fact: - superuser_password: "{{ lookup('password', secrets_store ~ '/' ~ inventory_hostname ~ '/SAREX_SUPERUSER_PASSWORD chars=ascii_letters,digits length=' ~ secret_length) }}" - when: sarex_create_superuser | default(false) | bool - no_log: true - tags: [superuser] - -# Backend теперь живёт в k3s, а не в compose, поэтому идём через kubectl exec -# внутри контейнера k3s-server, а не через docker compose exec. +# --- Django-суперпользователь ---------------------------------------------- +# Самого создания здесь больше нет — его делает под backend при каждом старте +# (apps/django/aero/kustomization.yaml, args стартового скрипта). Так учётка +# появляется вместе с приложением, а не отдельной командой оператора, и +# переживает пересоздание пода без ручного вмешательства. # -# Пароль передаётся переменной окружения (env в kubectl exec), а не аргументом -# командной строки: аргументы видны в ps любому, кто имеет доступ к ноде. -# Метка именно app=backend: universal-chart не проставляет -# app.kubernetes.io/instance, и селектор по нему возвращает пустой список. -- name: Найти под backend в k3s - when: sarex_create_superuser | default(false) | bool - ansible.builtin.command: - cmd: >- - {{ compose_cmd }} exec -T k3s-server - kubectl -n {{ django_namespace }} get pods -l app=backend - --field-selector=status.phase=Running - -o jsonpath={.items[0].metadata.name} - chdir: "{{ deploy_dir }}" - register: backend_pod - changed_when: false - tags: [superuser] - -# argv, а не shell: аргументы передаются процессу напрямую, без разбора -# шеллом. Иначе python-однострочник попадает внутрь двойных кавычек внутри -# sh -ec '...' и разваливается на вложенном экранировании. -# -# Пароль идёт переменной окружения, а не аргументом: аргументы видны в ps -# любому, у кого есть доступ к ноде. -- name: Создать/обновить Django-суперпользователя в backend (k3s) - when: sarex_create_superuser | default(false) | bool - ansible.builtin.command: - argv: - - "{{ compose_cmd.split()[0] }}" - - compose - - exec - - -T - - k3s-server - - kubectl - - -n - - "{{ django_namespace }}" - - exec - - "{{ backend_pod.stdout | trim }}" - - -c - - backend - - -- - - env - - "SU_NAME={{ superuser_name }}" - - "SU_PASS={{ superuser_password }}" - - "SU_EMAIL={{ superuser_name }}@{{ platform_domain }}" - - python - - manage.py - - shell - - -c - - >- - import os; - from django.contrib.auth import get_user_model; - U = get_user_model(); - u, c = U.objects.get_or_create(username=os.environ['SU_NAME'], defaults={'email': os.environ['SU_EMAIL']}); - u.is_staff = True; u.is_superuser = True; u.set_password(os.environ['SU_PASS']); u.save(); - print('created' if c else 'updated') - chdir: "{{ deploy_dir }}" - register: superuser_result - changed_when: true - no_log: true - tags: [superuser] - -- name: Показать учётные данные суперпользователя +# Ansible остаётся владельцем самих значений: генерирует их выше (тег env) и +# кладёт в Vault. Здесь — только показать логин по явному запросу +# (uv run poe superuser передаёт sarex_create_superuser=true). +- name: Показать учётные данные администратора платформы ansible.builtin.debug: msg: "Django superuser → login: {{ superuser_name }} password: {{ superuser_password }}" when: sarex_create_superuser | default(false) | bool diff --git a/apps/django/aero/kustomization.yaml b/apps/django/aero/kustomization.yaml index e10750d..65828eb 100644 --- a/apps/django/aero/kustomization.yaml +++ b/apps/django/aero/kustomization.yaml @@ -83,3 +83,61 @@ patches: - op: replace path: /spec/values/services/backend/envs/2/value/_default value: "False" + + # --- Администратор платформы -------------------------------------------- + # Учётка приезжает из Vault под именами, которые ждёт штатная + # createsuperuser --noinput. Секрет кладёт ansible + # (vault_app_secrets, secrets/apps/django/superuser). + - op: add + path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-secret-django-superuser + value: secrets/data/apps/django/superuser + - op: add + path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-template-django-superuser + value: |- + {{- with secret "secrets/data/apps/django/superuser" -}} + DJANGO_SUPERUSER_USERNAME={{ index .Data.data "username" }} + DJANGO_SUPERUSER_EMAIL={{ index .Data.data "email" }} + DJANGO_SUPERUSER_PASSWORD={{ index .Data.data "password" }} + {{- end -}} + + # Стартовый скрипт целиком, а не точечная правка: JSON6902 умеет заменять + # только элемент списка, а вся логика запуска — один shell-блок args[0]. + # Отличия от base: + # * не подключается django-kafka — этого секрета в контуре нет; + # * добавлены migrate и создание администратора. + # + # Почему migrate здесь, хотя его же делает /opt/sarex/entrypoint.sh: + # там он запущен без set -e, и его падение НИКАК не мешает uwsgi + # стартовать. Именно так контур несколько часов отдавал 200 на /admin/ + # с пустой схемой БД, пока backend не мог аутентифицироваться. Здесь + # команда выполняется под set -e из command базы (/bin/sh -ec), поэтому + # неудача валит под в CrashLoopBackOff — то есть становится видимой. + # Повторный прогон внутри entrypoint.sh при уже накатанной схеме + # отрабатывает вхолостую за секунды. + - op: replace + path: /spec/values/services/backend/deployment/args/_default/0 + value: | + set -a + [ -f /vault/secrets/django-postgresql ] && . /vault/secrets/django-postgresql + [ -f /vault/secrets/django-rabbitmq ] && . /vault/secrets/django-rabbitmq + [ -f /vault/secrets/django-s3 ] && . /vault/secrets/django-s3 + [ -f /vault/secrets/django-superuser ] && . /vault/secrets/django-superuser + [ -f /vault/secrets/django-jwt-private ] && export JWT_PRIVATE_KEY="$(cat /vault/secrets/django-jwt-private)" + [ -f /vault/secrets/django-jwt-public ] && export JWT_PUBLIC_KEY="$(cat /vault/secrets/django-jwt-public)" + set +a + + python manage.py migrate --noinput + + # Идемпотентно по построению: команда отрабатывает при КАЖДОМ старте + # пода, а на уже существующем логине штатно возвращает + # "That username is already taken" с ненулевым кодом. Ошибка ожидаема + # и гасится веткой else, иначе set -e ронял бы под на втором запуске. + if [ -n "${DJANGO_SUPERUSER_USERNAME:-}" ]; then + if python manage.py createsuperuser --noinput; then + echo "entrypoint: администратор создан" + else + echo "entrypoint: администратор уже существует, пропускаем" + fi + fi + + exec /opt/sarex/entrypoint.sh