Skip to content

📦 Пакетный менеджер и реестры ​

📌 Описание ​

Пакетный менеджер в духе apt/homebrew: приложения описываются манифестом asc.yaml, публикуются в реестрах, ставятся командой asc install <package>. Реестры бывают официальные и кастомные, локальные (file://) и удалённые (https://), включая GitHub-репозитории.

🎯 Сценарии использования ​

  • asc install nginx — установка из официального реестра.
  • asc source add https://registry.example.com — подключение кастомного реестра (как apt source).
  • asc source add https://github.com/user/my-app — приложение прямо из GitHub (asc.yaml в корне); для приватного при 404 — предложение подключить токен.
  • asc install https://github.com/user/my-app --branch dev — установка прямо из URL репозитория, вообще без реестра (DMN-040): для форков и пакетов, которые никогда не публиковались. asc source add выше подключает репозиторий к реестрам насовсем; это — для разовой пробы.
  • Магазин платформы (🛍️ app-store) — витрина над теми же реестрами.

🏗️ Техническое решение ​

Манифест asc.yaml (черновик) ​

yaml
name: my-app
version: 1.2.0
type: docker | native | utility
category: web                 # тема для реестра (databases, ai, bots, game-servers…)
description: "..."            # EN в официальном реестре
settings: ./asc.settings.yaml # необязательно: файл настроек приложения (см. ниже)
runtime:
  image: nginx:1.27           # для docker
  stdin: true                 # docker, необязательно: держать stdin открытым (docker run -i) — ввод из `asc attach` доходит до приложения
  tty: true                   # docker, необязательно: выделить псевдо-TTY (docker run -t)
  # или install/start/stop команды для native
requirements: { ram: 256M, disk: 1G }
healthcheck: { http: /health }

ℹ️ В манифесте нет секций env:, ports: и volumes: (DMN-027/030): environment-переменные, публикуемые порты и тома объявляются в asc.settings.yaml — настройками с ключом env: и типами ports / volumes (см. ниже). Один источник истины: что пользователь может настроить — то приложение и получает.

ℹ️ Каждый контейнер type: docker получает повышенный лимит открытых файлов (ulimit nofile, 10240 soft/hard) вместо дефолта самого движка в 1024 — некоторые игровые серверы (задокументированный случай — EOS SDK в 7 Days to Die) виснут или падают при старте на этом дефолте, причём без внятного симптома, кроме тишины в логе после первой же строчки.

🏗️ Локальная сборка образа: image-build (DMN-050) ​

Приложение type: docker может поставлять собственный Dockerfile и собирать образ локально движком вместо (или наряду с) скачиванием готового:

yaml
runtime:
  image-build:
    context: .              # каталог контекста сборки относительно манифеста (по умолчанию '.')
    dockerfile: Dockerfile  # относительно контекста (по умолчанию 'Dockerfile')
    args:                   # необязательные значения --build-arg
      VERSION: "1.0"
    tag: asc-local/my-app   # необязательно; по умолчанию 'asc-local/<app>:latest'
  • Только image → скачивается, как раньше.
  • Только image-build → образ собирается из Dockerfile пакета при установке (и пересобирается при upgrade / пересоздании контейнера из-за дрейфа настроек; кэш слоёв делает это дёшево). Контекст сборки упаковывается из репозитория пакета и не выходит за его пределы.
  • И image, и image-build → установщик предлагает выбор: интерактивно asc install <app> печатает два варианта и спрашивает; неинтерактивно (или чтобы пропустить вопрос) передайте --image (скачать готовый) или --build (собрать локально). Выбранный источник записывается в meta.json, чтобы последующее пересоздание или upgrade использовали тот же вариант без повторного вопроса.

⚠️ Сборка выполняется от демона (root). До появления пер-пользовательской политики контейнеров (DMN-043) сборка рассчитана на доверенные пакеты; базовый образ из FROM движок тянет анонимно (приватные базовые образы для локальной сборки — следующий инкремент).

Сборка всегда идёт через бэкенд BuildKit движка, а не через старый билдер — синтаксис Dockerfile вроде COPY --chmod/--chown требует BuildKit, иначе старый билдер падает с ошибкой «the --chmod option requires BuildKit». В терминале каждый шаг сборки отображается отдельным прогресс-баром в стиле docker build (как и полосы по слоям при docker pull), независимо от уровня логирования.

📐 JSON-схемы манифестов: asc.schema.json, asc.stack.schema.json и asc.settings.schema.json в репозитории registry.

Настройки приложения: asc.settings.yaml ​

Настройки приложения выносятся в отдельный файл, на который ссылается asc.yaml (settings: ./asc.settings.yaml). В нём описываются параметры: тип, лимиты, перечисления, значения по умолчанию — при установке пользователь заполняет их (UI платформы рисует форму, CLI задаёт вопросы; в silent-режиме берутся дефолты):

yaml
settings:
  - key: server_name
    type: string                # string | number | boolean | enum | secret
    title: "Server name"
    default: "My Server"
    required: true
    env: SERVER_NAME            # пробрасывается приложению этой env-переменной

  - key: max_players
    type: number
    default: 10
    limits: { min: 1, max: 200 }   # лимиты значений

  - key: difficulty
    type: enum                  # перечисление
    values: [peaceful, easy, normal, hard]
    default: normal

  - key: game_version
    type: enum                  # пресеты, но допускается и своё значение
    values: [public, latest_experimental]
    default: public
    allow_custom: true           # любая другая ветка/build id принимается как есть

  - key: rcon_password
    type: secret                # хранится как секрет, маскируется
    required: true

  - key: enable_backups
    type: boolean
    default: true

  - key: game_port
    type: ports                 # публикуемые порты контейнера (список)
    default: [27015]
    limits: { min: 1024, max: 65535 }
    env: CS2_PORT               # в env — через запятую; один порт — как есть
    protocol: both               # tcp (по умолчанию) | udp | both

  - key: game_data
    type: volumes               # тома приложения (список, формы — ниже)
    default: [/home/steam/cs2-dedicated]
  • Значения настроек сохраняются в /asc/apps/<id>/config/settings.json (0600 — файл может содержать секреты); при установке заполняются дефолтами, при обновлении новые ключи получают дефолты, а выбор пользователя не трогается.
  • Проброс в env: каждая настройка с env: VAR_NAME попадает в окружение приложения (секреты тоже — для этого их env: и нужен). Для Docker-приложений переменные записываются в env контейнера при создании. Списковые значения (ports, volumes) пробрасываются через запятую.
  • type: ports — публикуемые порты контейнера (порт хоста == порту контейнера). Настройка с env: держит приложение и Docker синхронными: сервер слушает ровно там, куда проброшен порт. protocol задаёт транспорт: tcp (по умолчанию), udp или both (один и тот же порт хоста==контейнера пробрасывается и на TCP, и на UDP).
  • type: volumes — тома приложения; каждая запись — одна из трёх форм:
    • /путь/в/контейнере — приватные данные приложения: в этот путь контейнера монтируется папка data приложения (/asc/apps/<id>/data). Папка создаётся world-writable (0777): образы работают от произвольных непривилегированных пользователей, а bind-монтирование сохраняет владельца хоста; каталог приложения выше остаётся закрытым. Если образ объявляет числовой USER uid:gid, папка дополнительно chown'ится под него (DMN-038) — непривилегированный процесс может сделать chown только пути, которым уже владеет, поэтому образу, который при первом старте сам меняет владельца своей папки данных (а не только пишет в неё), без этого падает с EPERM; именованный (steam) или голый числовой USER без группы оставляет только world-writable — его группа известна лишь /etc/passwd самого образа;
    • /путь/в/контейнере:хост — то же, но вместо data используется указанное после двоеточия: простое имя папки — внутри каталога приложения (/asc/apps/<id>/<папка>; repository, config и meta.json зарезервированы), абсолютный путь — путь хостовой машины, монтируется как есть (существующий каталог сохраняет владельца и права);
    • имя:/путь/в/контейнере[:ro|:rw] — именованный том Docker, Engine создаёт его при первом использовании. Именованные тома — способ разделить данные между приложениями: одно приложение пишет в том, остальные монтируют его :ro (см. стек cs2 в asc-example-apps). Именованные тома не удаляются вместе с приложением.
  • Применение изменений: конфигурация контейнера фиксируется при создании, поэтому при следующем asc app start / asc app restart демон сравнивает желаемое состояние — env, публикуемые порты, тома, квоту, команду запуска — с фактическим состоянием контейнера и пересоздаёт контейнер при расхождении (или если контейнер пропал). Данные приложения живут в томах и переживают пересоздание. Если желаемое состояние вычислить не удалось (например, источник реестра удалён) — приложение всё равно стартует как есть, с предупреждением в логе: доступность важнее.
  • Изменение настроек — asc app settings <id>: интерактивный редактор в терминале. Сначала показываются категории — environments (настройки string/number/boolean/enum/secret), ports, volumes, quota, start_command — затем настройки выбранной категории: выбираете по номеру, вводите значение, оно валидируется по описанию (тип, limits, значения values у enum; секреты в списке маскируются; порты и тома вводятся списком через пробел). Также через UI платформы. После изменения — перезапуск приложения (asc app restart <id>).
  • allow_custom (только type: enum) — принимает любое значение вне списка values как свободный текст, вместо отклонения. Используйте для перечислений, где перечислены частые пресеты, но пользователя не должно ограничивать значение, которое автор не предусмотрел (ветка/build id игры, своё имя мира). Пронумерованный список в asc app settings по-прежнему показывает пресеты; ввод чего-то другого просто принимается.
  • Доступ через API (DMN-078): GET/PUT /v1/apps/{id}/settings через REST API демона и эквивалент AppService.GetAppSettings/SetAppSettings через gRPC — те же значения, что и в CLI-редакторе, values_json/values — JSON-объект, а не google.protobuf.Struct (который превратил бы целое число вроде порта в double). Запись возвращает restart_required: true, если приложение сейчас запущено и его фактическая конфигурация теперь разойдётся с сохранёнными значениями — вызывающему стоит предложить asc app restart <id> (или собственное действие рестарта платформы); у остановленного приложения — всегда false, значения просто применятся при следующем запуске.

🔌 Доставка групп Environment ($env) ​

Рядом с зарезервированными ключами $quota/$start_command/$backup settings.json принимает четвёртый: $env — объект произвольных пар ИМЯ: значение (строки). Он существует для групп Environment платформы (наборы переменных на уровне организации/проекта — 🌱 environments): группа, подключённая к приложению, разрешается и сливается платформой, а затем записывается под этим ключом так же, как любая другая настройка — то есть едет по тому же конвейеру settings.json → применение при (пере)запуске, без отдельного канала доставки.

json
{ "$env": { "BOT_TOKEN": "…", "TZ": "Europe/Moscow" } }
  • Имена должны соответствовать ^[A-Za-z_][A-Za-z0-9_]*$; некорректная запись отклоняется при сохранении значений, как и у любой другой настройки.
  • Приоритет: настройка пакета с совпадающим ключом env: — и реальным значением — всегда побеждает одноимённую запись в $env. Пользователь явно настроил это значение в собственных настройках приложения — более осознанный и специфичный выбор, чем общий дефолт уровня организации/проекта. Запись $env без соответствующей настройки пакета доходит до env контейнера безусловно.
  • Интерактивный редактор asc app settings её не показывает (ей управляет платформа), но и не трогает — как любой незнакомый ему ключ.

📏 Квоты ресурсов (quota) ​

Секция quota: в asc.settings.yaml ограничивает ресурсы инстанса приложения (DMN-021):

yaml
quota:
  max_cpu: 2        # лимит CPU в ядрах (0.5, 2, …)
  max_ram: 1G       # лимит памяти: 512M, 2G, … (двоичные единицы, как docker -m)
  max_disk: 10G     # лимит места на диске
  • Значения нормализуются при установке/обновлении и записываются в meta.json; asc app info <id> показывает их (quota: cpu ≤ 2, ram ≤ 1.0 GiB, …).
  • Docker-приложения: применяются при создании контейнера через Engine API (NanoCpus, Memory).
  • native/process: записываются в meta.json; применение через cgroups — следующий инкремент.
  • max_disk записывается для всех рантаймов; дисковое ограничение per-runtime (Docker storage-opt / квоты ФС) — инкрементально.
  • Пользовательские переопределения (DMN-030): категория quota в asc app settings переопределяет отдельные поля поверх значений пакета ('-' сбрасывает поле обратно); переопределение хранится в settings.json и применяется через пересоздание контейнера при следующем перезапуске.

⚠️ Проверка нехватки ресурсов и --force (DMN-099) ​

Перед тем как что-либо скачивать или собирать, демон сравнивает requirements: манифеста и эффективную quota: с тем, что реально есть на хосте. Нехватка вызывает типизированную ошибку pkg::resources::RequirementsNotMet вместо продолжения — установка cs2 (requirements: { ram: 4G, disk: 80G, cpu: 2 }) на ноду с 1 ядром/1 ГиБ раньше падала где-то внутри docker create сырой ошибкой движка (Docker responded with status code 400: range of CPUs is from 0.01 to 1.00, as there are only 1 CPUs available); теперь она падает сразу с installing 'cs2' needs more than the host has right now: RAM 4.0 GiB > 0.9 GiB, disk 80.0 GiB > 14.0 GiB, CPU 2 > 1.

  • Что сравнивается: RAM/диск — с тем, что сейчас свободно на хосте (уже установленные приложения считаются, то же правило, что уже использует советующая проверка asc app start); CPU — с полным числом логических ядер хоста, а не с текущей загрузкой — ядра не расходуются так, как RAM, и именно полная ёмкость предсказывает собственную валидацию движка. Цифра CPU — это максимум из requirements.cpu и эффективной quota.max_cpu: именно квота реально доходит до NanoCpus, поэтому её превышение важно, даже если requirements.cpu вообще не задан (пакет только с quota:, без секции requirements:, тоже сработает на проверке).
  • --force (asc install <pkg> --force, InstallAppRequest.force) пропускает проверку и ставит принудительно. В CLI интерактивный путь печатает, чего не хватает, и спрашивает «Всё равно установить на свой страх и риск? [y/N]» вместо требования флага заранее (неинтерактивные вызовы получают структурированную ошибку и решают сами).
  • CPU-квота всё равно обрезается до полного числа ядер хоста, независимо от --force. Квота выше nproc — это не риск, который человек может принять на себя: движок жёстко её отклоняет, поэтому эффективный NanoCpus, уходящий в docker create, молча обрезается до числа ядер хоста, если превышает его, с предупреждающей строкой (в живом выводе asc install либо в логе демона для вызывающего без канала прогресса), называющей потолок. RAM и диск никогда не обрезаются: ничего ниже по стеку их не проверяет против ёмкости хоста так, как движок проверяет CPU, так что --force действительно означает «поставить по-своему».
  • Форма API: зеркалит форму согласия с лицензией (LicenseRequired) один в один — InstallAppResponse.requirements_not_met (RequirementsNotMetDetail{ app, shortages: [{ resource, need, have }] }) вместо ошибки gRPC, поэтому диалог установки платформы рисует свой экран «не хватает ресурсов, установить всё равно?» и повторяет запрос с force = true — тот же приём, что уже применяется для согласия с лицензией.

🚀 Команда запуска (start_command) ​

Команда запуска приложения настраивается в asc.settings.yaml. В строку можно подставлять environment-переменные пакета — синтаксис ${VAR}:

yaml
start_command: "steamcmd +force_install_dir /data +login anonymous +app_update ${STEAM_APP_ID} validate +quit"
  • Подстановка выполняется демоном при установке/обновлении из env приложения — значений настроек с ключами env:, включая дефолты. Неразрешённая переменная — ошибка установки с указанием имени переменной.
  • Docker-приложения: команда заменяет то, что запустил бы образ (entrypoint становится /bin/sh -c, поэтому аргументы и кавычки работают как в shell).
  • native-приложения: команда переопределяет runtime.start из asc.yaml.
  • Подстановка разрешает ${VAR} из всего вычисленного окружения приложения — значений настроек с ключами env: и любых записей $env от подключённой группы Environment (см. выше), так что команда запуска может ссылаться и на переменную, доставленную группой — если та попала в settings.json до (пере)запуска, который пересчитывает команду. Предпросмотр вычисленной команды в UI платформы — следующий инкремент (🌱 environments).
  • Пользовательское переопределение (DMN-030): категория start_command в asc app settings заменяет команду пакета для этого инстанса ('-' сбрасывает обратно); ссылки ${VAR} разрешаются из env настроек при применении.

🐳 Скрипты install/update: native или docker ​

Скрипты install и update пакета могут исполняться как нативно на хосте, так и в docker — управляется полем run_in в блоке scripts: манифеста:

yaml
scripts:
  install:
    run: ./scripts/install.sh
    run_in: native            # native | docker
  update:
    run: ./scripts/update.sh
    run_in: docker            # одноразовый контейнер
    image: debian:12          # образ для docker-исполнения (опционально)
  • native — скрипт исполняется на хосте от пользователя приложения.
  • docker — скрипт исполняется в одноразовом контейнере с примонтированным каталогом /asc/apps/<id>/ (изоляция зависимостей сборки от хоста).
  • По умолчанию run_in наследует type пакета: docker → docker, native/utility → native.

Механика установки: клонирование репозитория ​

Установка приложения = клонирование его репозитория:

  1. asc install <package> → демон клонирует репозиторий пакета в /asc/apps/<id>/repository/.
  2. Версии приложения = git-теги (GitHub tags), берутся из репозитория, а не из реестра (DMN-047): установка конкретной версии — asc install <package>@1.2.0 (checkout тега); asc install <package> без версии резолвит последний тег репозитория через git ls-remote (нет тегов → HEAD ветки по умолчанию); asc install <package>@ (пустой @) показывает теги и ветки репозитория для интерактивного выбора (DMN-048), либо неинтерактивно возвращает список доступных версий ошибкой. Обновление — asc app upgrade <name> (checkout нового тега). Поэтому индекс реестра не хранит поле версии — версии пакета это то, чем протегирован его репозиторий. Вызывающим через API тот же список тегов доступен без всего этого — AppService.ListAppVersions (DMN-0XX) принимает голый git URL и возвращает его теги (сначала новые) плюс какой из них latest, один круг git ls-remote, без установки/клонирования: им пользуется выбор версии на платформе (мастер установки, выпадающий список при обновлении).
  3. Согласие с лицензией (DMN-028, DMN-032, DMN-091): если клонированный пакет содержит лицензию (LICENSE.md / LICENSE / LICENSE.txt), CLI показывает, откуда ставится пакет (источник реестра + репозиторий), выводит текст лицензии и спрашивает согласие — отказ прерывает установку, не оставляя следов. Лицензия сначала ищется в каталоге самого пакета (пакет монорепозитория может иметь свою лицензию), затем в корне репозитория. Неинтерактивный ввод принимает лицензию автоматически с печатным уведомлением. Вызовы через API получают те же поля (package, source, git, license) структурно, но по-разному в зависимости от транспорта: локальный REST API (тот же unix-сокет, которым пользуется CLI) отдаёт их полем license_required в ответе 409; по gRPC (AppService.InstallApp/InstallAppStream, которым пользуется платформа) это вообще не ошибка — InstallAppResponse несёт необязательное сообщение license_required вместо id/version, поскольку вызов по сути успешен и нужен лишь ещё один запрос с license_ack = true. UI платформы в обоих случаях отрисует свой диалог согласия из этих полей. Для стека вопрос задаётся один раз на репозиторий. Пакеты без файла лицензии ставятся без вопроса.
  4. Пользовательское название и несколько инстансов (DMN-024, DMN-033): в терминале asc install спрашивает название приложения — Enter оставляет значение по умолчанию, любой другой ввод становится названием приложения. Неинтерактивно — флаг --name "My Server". Установка уже установленного пакета больше не завершается ошибкой: создаётся новый инстанс со следующим свободным id (<пакет>-2, <пакет>-3, …), который становится и его custom_name, если --name его не переопределяет. Название хранится в meta.json (custom_name), должно быть уникально среди приложений пользователя, переживает обновления, и все команды принимают его наравне с id. Суффиксированный инстанс запоминает пакет реестра, из которого он поставлен (meta.package), поэтому asc app upgrade продолжает резолвить его корректно.
  5. Имена нечувствительны к регистру, id — в нижнем регистре (DMN-059): id приложения становится каталогом, именем контейнера и юнитом systemd, поэтому ограничен набором [a-z0-9_-] — но пользователя это касаться не должно. Везде, где имя выводится (имя репозитория при прямой установке по URL, имя записи реестра, поле name: в asc.yaml / asc.stack.yaml), оно приводится к этому виду: HOMEBAR ставится как homebar, Home.Bar — как home-bar. Везде, где имя вводится, оно сравнивается без учёта регистра — asc install HOMEBAR, asc install mystack/MyApp, asc app start HOMEBAR и asc app logs homebar попадают в одно и то же приложение. Репозиторий, который ставится только после переименования автором, — это баг, а не правило.
  6. Дальше демон работает с локальной копией: читает asc.yaml/asc.settings.yaml, собирает/запускает по типу приложения.
  7. Для Docker-приложений контейнер создаётся через Engine API; отсутствующий на хосте образ скачивается автоматически из его реестра (runtime.image; имя без тега означает latest) — и при установке, и при обновлении.
  8. Прогресс-бары: в терминале клонирование репозитория (git clone --progress) и скачивание Docker-образа отображаются живыми полосами прогресса (в стиле docker pull/docker-compose pull — по одной полосе на слой образа) — включены по умолчанию, независимо от asc config debug. При неинтерактивном вызове (перенаправленный вывод, API демона) полос нет — те же события всегда доступны как debug-логи (asc config debug on).

Чтение пакета без клонирования (DMN-131): AppService.InspectPackage — то, что диалог установки платформы вызывает до установки, — никогда не скачивает содержимое пакета. Он делает разреженный снимок без blob'ов: git clone --depth 1 --filter=blob:none --no-checkout скачивает последний коммит и его деревья (список файлов без содержимого), core.sparseCheckout ограничивает рабочее дерево файлами, из которых читается пакет, — asc.yaml, asc.stack.yaml, asc.settings.yaml (и файл настроек по нестандартному пути settings: — докачивается по требованию), LICENSE* каталога пакета и корня репозитория, а также то, что читает детектор способов установки (compose-файлы, Dockerfile*, Chart.yaml, YAML до трёх уровней вглубь), — и git read-tree -mu HEAD одним пакетом докачивает только их blob'ы. Репозиторий с гигабайтами ассетов стоит несколько килобайт на одну проверку. Используется core.sparseCheckout с .git/info/sparse-checkout, а не git sparse-checkout set --no-cone: тому нужен git 2.35 — новее, чем в Debian 11 / Ubuntu 20.04. Сервер без поддержки partial clone игнорирует фильтр и отдаёт blob'ы целиком: снимок вырождается в прежний shallow-клон, но из-за этого не падает. Приватные репозитории идут через те же учётные данные (asc auth), в том числе при ленивой докачке blob'ов. Это же чтение отвечает на два вопроса, которые установка раньше узнавала, только провалив первый прогон: InspectPackageResponse.license несёт текст лицензии, которую установка попросит принять (те же поля, что у license_required), а requirements_not_met — нехватку ресурсов на хосте прямо сейчас (та же проверка, что InstallApp делает без force, только заранее; для стека — первое обязательное приложение, которое не помещается). Установщик спрашивает оба ответа заранее и запускает одну настоящую установку с уже решёнными license_ack/force. Прямая установка из git тем же снимком отличает приложение от стека до своего единственного полного клона, а не клонирует сначала как приложение и перезапускается стеком.

Requirements при запуске (DMN-029): requirements манифеста (ram, disk, cpu) сравниваются с тем, что свободно на хосте в момент запуска приложения. При нехватке asc app start предупреждает с точными цифрами и — в терминале — спрашивает, запускать ли всё равно на свой страх и риск; неинтерактивные вызовы получают предупреждение в stderr и продолжают. Проверка — совет, а не запрет: ошибки чтения никогда не блокируют запуск.

Обновление (asc app upgrade <name>[@версия], синоним — asc upgrade): приложение должно быть остановлено; новый тег клонируется рядом с текущей копией (repository.new), манифест валидируется, и только затем каталоги меняются местами и рантайм пересоздаётся (для Docker — контейнер пересоздаётся с новым образом). Ошибка до подмены не трогает установленное приложение; ошибка пересоздания рантайма откатывает на предыдущую версию. Без явной версии берётся последний тег репозитория (DMN-047); репозиторий без тегов нельзя обновить без явной @версии. Если уже стоит запрошенная (или последняя) версия, об этом так и сообщается — без фиктивного обновления. Через API (DMN-0XX/NODE-022) AppService.UpgradeApp повторяет InstallApp — spec это id приложения, опционально @версия, — а UpgradeAppStream повторяет InstallAppStream один в один (событие line на каждый шаг, финал тем же результатом, что вернул бы UpgradeApp): демон по-прежнему требует, чтобы приложение было уже остановлено, поэтому фасад платформы сам его останавливает, стримит обновление и перезапускает, если оно было запущено — сам демон это не оркестрирует.

Прямая установка из git-репозитория (asc install <url> [--branch <имя>|--tag <имя>] [--path <подкаталог>], DMN-040): если спецификация — git-URL (https://, ssh:// или scp-подобный git@хост:путь), а не имя пакета, демон полностью пропускает реестр и клонирует репозиторий напрямую — тот же конвейер клонирование/манифест/провижининг, что и при установке из реестра, минус шаг резолвинга. По умолчанию asc.yaml ищется в корне репозитория; --path <подкаталог> (DMN-096) указывает, где искать манифест внутри клона — тот же смысл, что у source.path записи реестра, просто эту установку некому резолвить за пользователя. --branch/--tag выбирают ref для checkout; без флагов клонируется HEAD ветки по умолчанию. Id приложения по умолчанию — при заданном --path последний сегмент пути, приведённый к каноническому нижнему регистру (asc install https://github.com/AdminServiceCloud/asc-example-apps --path web/helloworld ставится как helloworld, а не asc-example-apps), иначе имя самого репозитория (bar и для https://github.com/foo/bar.git, и для git@github.com:foo/bar, homebar для https://github.com/mireblood/HOMEBAR, см. пункт 5 выше); --name переопределяет id в обоих случаях. Путь запоминается в meta.json (repo_path) и переживает asc app upgrade — без него обновление монорепо-пакета сломалось бы так же, как его установка без --path. Приватные репозитории используют ровно ту же авторизацию asc auth, что и ниже — установка по URL это установка из реестра с выброшенным шагом резолвинга. asc upgrade резолвит приложения, установленные так, по записанному URL (DMN-053): установка по тегу переходит на последний тег репозитория, установка с --branch следует за своей веткой. Повторный asc install с тем же URL по-прежнему создаёт новый инстанс, а не обновляет существующий.

Это же поле доступно вызывающим через API: платформа устанавливает пакеты кастомных реестров именно так — голым git URL пакета, не именем через реестр демона (чтобы установка не зависела от того, выложен ли реестр организации на конкретную ноду) — и до DMN-096 установка любого монорепо-пакета (несколько приложений в одном репозитории) с платформы падала: path терялся на границе InstallAppRequest, демон искал asc.yaml в корне клона и называл приложение по репозиторию. InstallAppRequest.path (unary InstallApp и стрим InstallAppStream) — то же поле, что и флаг CLI, только не в spec-строке, а отдельным аргументом.

Учётные данные (DMN-045/046): единый пользовательский стор хранит авторизацию и для приватных репозиториев (git clone), и для приватных реестров образов (pull через Engine), различая их полем type (repo | registry). Записи ключуются на хост или префикс (github.com/myorg, ghcr.io/myorg) и хранятся отдельно от списков источников, в JSON: /etc/asc/auth.json (root) и ~/.asc/auth.json (пользователь, рядом с остальным пер-пользовательским деревом DMN-041), оба файла 0600. Секреты не попадают ни в world-readable файлы, ни в argv git-процессов (argv виден всем пользователям через /proc).

json
{
  "credentials": [
    { "type": "repo",     "pattern": "github.com/myorg", "token": "ghp_xxx" },
    { "type": "registry", "pattern": "ghcr.io/myorg", "username": "me", "token": "ghp_yyy" },
    { "type": "repo",     "pattern": "github.com/myorg/secret-app", "app": "6f8a…uuid", "key": "/home/me/.ssh/id_ed25519" }
  ]
}
  • Типы: repo авторизует git clone — токен для https://-URL (через GIT_ASKPASS + переменную окружения процесса git) или SSH-ключ для git@/ssh://-URL (GIT_SSH_COMMAND c -i <ключ> -o IdentitiesOnly=yes). URL следует за учётными данными (DMN-059): учётные данные авторизуют только свой транспорт, поэтому клонирование https://github.com/org/app при настроенном для этого хоста SSH-ключе уходит по ssh (git@github.com:org/app.git), а ssh-URL при настроенном токене — по https. Без такой замены настроенные учётные данные просто игнорировались бы, и клонирование падало бы так, будто ничего не настраивали. registry авторизует pull образа — токен вместе с username, уходит в Docker Engine заголовком X-Registry-Auth (с реестром связывается Engine, а не демон, поэтому TLS-стек на стороне демона не нужен). Registry-запись отвергает SSH-ключ и требует --username.
  • Привязка к приложению (app, DMN-044): учётные данные можно ограничить одним приложением по его uuid или id (--app) — токен обслуживает ровно нужное приложение и никакое другое. Привязанная запись невидима для остальных приложений и для «своего» приложения побеждает равную по длине непривязанную; без app учётные данные применяются ко всем приложениям, чей URL/образ совпал с префиксом. В остальном правило совпадения одинаково для обоих типов: выбирается самый длинный префикс на границе пути, записи пользователя приоритетнее системных. Шаблоны сравниваются без учёта регистра (DMN-059) — имена хостов регистронезависимы по определению, и форджи так же относятся к именам владельца и репозитория, поэтому ключ, добавленный для github.com/MyOrg, авторизует клонирование github.com/myorg/app и наоборот.
  • Детекция: git всегда запускается с GIT_TERMINAL_PROMPT=0 и BatchMode=yes — клонирование приватного репозитория без настроенной авторизации не подвисает на запросе пароля, а падает с распознаваемой ошибкой (включая «Repository not found», который GitHub отдаёт для приватных репозиториев без доступа). Pull приватного образа показывает собственную ошибку авторизации Engine. Клонирование по ssh с хоста, которого нет в known_hosts пользователя, от имени которого работает asc (для демона — root), падает так же — в BatchMode некому задать вопрос «continue connecting?», — поэтому такая ошибка сразу несёт с собой подсказку ssh-keyscan <хост> >> ~/.ssh/known_hosts (DMN-059).
  • Интерактивная настройка: обнаружив приватный репозиторий, CLI в терминале спрашивает разрешение настроить авторизацию сразу: для ssh — выбор из найденных приватных ключей ~/.ssh, для https — токен либо тот же выбор ключа, если ключи есть и пользователь предпочёл их (клонирование тогда переключается на ssh-транспорт, см. Типы); выбор сохраняется, установка повторяется автоматически. Под sudo предлагаются ключи вызвавшего пользователя — его домашний каталог берётся из базы пользователей, а не из $HOME, который дистрибутивы переписывают по-разному. Неинтерактивный вызов (API, скрипты) получает структурированную ошибку с подсказкой asc auth add ....
  • Интерактивная настройка через демон (DMN-062): подсказка не ограничена in-process-путём. Когда установка идёт через демон — а это обычный случай, asc install сначала пробует сокет, — приватный репозиторий возвращается структурированной ошибкой auth_required (HTTP 409 с {"auth_required": {"url": …}}; намеренно не 401 — на TCP-слушателе этот код принадлежит bearer-токену API), CLI восстанавливает ту же типизированную ошибку, что поднимает in-process-клонирование, показывает тот же диалог и повторяет установку. То есть asc install https://github.com/<user>/<приватный-репо> сам предлагает выбор «ключ или токен», записывает его в asc auth и ставит приложение — одной командой, без отдельного захода в asc auth add.
  • Чьи учётные данные использует демон (DMN-062): демон работает от root, поэтому сам по себе видел бы только /etc/asc/auth.json — а обычный пользователь не может писать в этот файл, и без sudo диалог выше был бы бессмысленным. Вместо этого демон резолвит учётные данные от имени вызывающего: по uid пира (SO_PEERCRED либо приписанный SUDO_UID для root-пира) из базы пользователей берётся его домашний каталог, и его ~/.asc/auth.json читается как список с приоритетом выше системного. Запись с SSH-ключом из пользовательского стора обязана указывать на ключ, принадлежащий этому пользователю: демон открывает файл от root, и вручную отредактированный ~/.asc/auth.json со ссылкой на /root/.ssh/id_ed25519 не должен превращаться в способ клонировать ключом root; чужие и нечитаемые ключи игнорируются с предупреждением в журнале. Учётные данные реестров образов по-прежнему берутся только из системного стора.
  • Миграция: старые TOML-файлы (/etc/asc/git-auth.toml, ~/.config/asc/git-auth.toml) до DMN-045 по-прежнему читаются, если JSON-стора ещё нет — их записи без type грузятся как repo — и мигрируют в auth.json при первой записи через asc auth, так что настроенная авторизация переживает обновление.
  • CLI: asc auth add <хост|префикс> [--type repo|registry] --token <токен> [--username <user>] [--app <uuid|id>] · asc auth add <хост> --ssh-key [путь] (без пути — интерактивный выбор ключа) · asc auth list (типы и методы без секретов) · asc auth remove <хост> [--type repo|registry].
  • CredentialService: тот же стор, только под управлением по API вместо CLI — так платформа выкладывает credentials реестров организации на ноду, не запуская asc auth add по SSH. ListCredentials/UpsertCredential/RemoveCredential работают только с системным стором (/etc/asc/auth.json) — собственный ~/.asc/auth.json пользователя остаётся исключительно под CLI. UpsertCredential апсертит по (kind, pattern, app), как и GitAuth::add выше, и теперь принимает секрет любого из двух видов: токен либо сырые байты приватного ключа в PEM. Для ключа демон сам пишет его в 0600-файл под /etc/asc/ssh-keys и заводит Method::SshKey, указывающий на него, — ровно в том виде, что и asc auth add --ssh-key вручную. Замена или удаление такой записи удаляет и файл. ListCredentials никогда не возвращает сам секрет, включая файл ключа. Маркер владельца (managed_by): UpsertCredentialRequest.managed_by позволяет вызывающему помечать свои записи (платформа ставит "platform"), а RemoveCredentialRequest.managed_by, если задан, сужает удаление до записей ровно с этим маркером — выкладка платформы может перечислить свои учётные данные, сверить их с тем, что только что отправила, и удалить лишнее, ни разу не тронув запись, добавленную оператором вручную через asc auth add. asc auth list печатает managed_by=<значение> рядом с записью, у которой он есть.

Несколько приложений в одном репозитории: asc.stack.yaml ​

Правило корня: репозиторий-пакет может содержать сколько угодно asc.yaml в подкаталогах, но в его корне обязан лежать ровно один манифест — либо asc.yaml (одиночное приложение), либо asc.stack.yaml (стек), который соединяет все вложенные asc.yaml. Вложенные манифесты без корневого не индексируются.

Манифест-стек asc.stack.yaml перечисляет приложения и пути к их asc.yaml:

yaml
name: my-stack
version: 1.0.0
description: "..."
apps:
  - name: web
    path: ./web            # каталог с asc.yaml
  - name: worker
    path: ./worker
  - name: db
    path: ./db
    optional: true          # необязательный компонент стека
  • asc install my-stack — установка всего стека; asc install my-stack/web — только одного приложения из него.
  • Переименование стека при установке (DMN-034): asc install my-stack --name prod — это префикс, применяемый ко всем устанавливаемым приложениям — prod-web, prod-worker, prod-db — так как у стека нет собственной сущности, только приложения-участники. asc install my-stack/web --name prod-web называет только запрошенное приложение, как при установке одиночного приложения.
  • Стек может объявлять зависимости между приложениями (depends_on — порядок запуска), компоненты могут быть optional. Environment-переменные живут в asc.settings.yaml каждого приложения.
  • Реестры и магазин платформы индексируют как одиночные asc.yaml, так и стеки asc.stack.yaml.
  • Примеры — в репозитории asc-example-apps.

Механика установки стека: репозиторий клонируется один раз — для чтения asc.stack.yaml и как источник для всех участников (DMN-131): каждое выбранное приложение устанавливается как обычное, со своей копией этого клона в /asc/apps/<id>/repository/, своим каталогом и meta.json, но без повторного скачивания репозитория:

  • id приложения — это name из его собственного asc.yaml (в примере выше приложение web стека может называться my-stack-web); происхождение записывается в meta.json как package: "my-stack/web" — по нему asc app upgrade заново резолвит пакет.
  • Порядок: зависимости (depends_on) устанавливаются первыми; циклы отклоняются при валидации. asc install my-stack ставит все компоненты кроме optional; asc install my-stack/db ставит запрошенный компонент (в том числе optional) и его зависимости.
  • Повторная установка: запрошенное приложение (то, что указано явно, а не подтянутая зависимость), если оно уже установлено, становится новым инстансом (DMN-033) вместо пропуска; уже установленная зависимость по-прежнему переиспользуется, а не дублируется. Каждое приложение ставится атомарно (ошибка убирает только его каталог, ранее установленные компоненты остаются).
  • Согласие с лицензией запрашивается один раз на репозиторий (один репозиторий = одна лицензия), а не на каждое приложение стека.

Стек, установленный напрямую по git-URL (DMN-097): запись реестра объявляет свой type (app или stack), голый URL не объявляет ничего — поэтому asc install <url> [--path <подкаталог>] узнаёт это из клона. Если в каталоге пакета лежит asc.stack.yaml, а не asc.yaml, установка перезапускается как установка стека и ставит все необязательные приложения — ровно так же, как asc install my-stack; --app <имя> ставит одно приложение стека (эквивалент реестровой спецификации <стек>/<приложение> для прямой установки — слэш в URL не поместишь), а --name работает тем же префиксом, что и для реестрового стека. Каждое приложение записывает репозиторий в meta.source, а в meta.repo_path — свой собственный подкаталог манифеста (gameservers/cs2/server, не корень стека), поэтому asc app upgrade потом читает манифест приложения, а не asc.stack.yaml. Принадлежность к стеку тоже записывается (DMN-121): meta.package = "<стек>/<приложение>", где стек назван так же, как вся установка, — по подкаталогу манифеста или по репозиторию, если стек лежит в корне, — поэтому asc stacks и платформа группируют git-стек так же, как стек из реестра; обновление по-прежнему идёт по source и repo_path. Запись реестра, ошибочно помеченная как type: app для стека, теперь так и говорит (package 'cs2' is a stack of 2 app(s) (server, master), not a single application) вместо падения с cannot read manifest .../asc.yaml: No such file or directory.

⚠️ На это опирается диалог установки в платформе: он ставит любой пакет реестра по git-URL + path, а не по реестровой спецификации, поэтому до DMN-097 пакет с type: stack (например, cs2) из UI не устанавливался вообще.

Чтение пакета без установки (InspectPackage, DMN-098): что репозиторий на самом деле содержит — одно приложение (asc.yaml) или стек (asc.stack.yaml) с составом приложений, их будущими id, depends_on, флагами optional и объявленными requirements. Демон делает shallow-клон во временный каталог, читает манифесты и удаляет клон: ничего не устанавливается, каталог приложения не создаётся. UI установщика вызывает это перед установкой, чтобы оператор видел, что пакет — стек, и какие приложения появятся на ноде: индекс реестра знает тип пакета, но не состав стека, а у прямой установки по URL записи в реестре нет вовсе. Только gRPC (AppService.InspectPackage); в CLI то же самое видно при установке или при чтении репозитория.

Детект всех способов установки репозитория (DMN-106): раньше репозиторий без asc.yaml/asc.stack.yaml просто ронял InspectPackage. Теперь он возвращает PACKAGE_KIND_UNSPECIFIED плюс все найденные в каталоге пакета способы установки — собственный манифест/стек ASC, compose-файл, голый Dockerfile, Swarm-стек, Kubernetes-манифесты или Helm-чарт — каждый со списком файлов-триггеров, флагом «устанавливается ли уже сегодня» и простой английской причиной, если нет. Compose-файл с блоком deploy: отдаёт сразу оба способа, DOCKER_COMPOSE и SWARM, — это один файл, применимый двумя способами. Обход файловой системы для этого ограничен (глубина 3, ~5000 записей, пропуск .git/node_modules/vendor/target/dist), а сам compose парсится разрешающе, а не строгим парсером с отказом на неизвестных полях, как asc.yaml, — compose это большой чужой формат, и то, чего демон не моделирует, не должно превращать «обнаружено» в «не удалось разобрать». Это же изменение чинит приватный репозиторий, всплывавший по gRPC голой внутренней ошибкой: InspectPackageResponse.auth_required/InstallAppResponse.auth_required теперь несут URL, его нормализованный шаблон и транспорт (https/ssh), который его авторизует, — тот же неаварийный формат, что уже есть у license_required/requirements_not_met.

Установка прямо из голого Dockerfile (DMN-107): репозиторий с одним Dockerfile и без ничего другого ставится как обычное type: docker-приложение — asc install <url> --method dockerfile синтезирует манифест и настройки в памяти (EXPOSE → опубликованные порты, VOLUME → тома, пара хост/контейнер точно как в написанном вручную asc.settings.yaml) вместо чтения их из репозитория, а всё, что идёт дальше (проверки лицензии/ресурсов, провижининг), не меняется. Ничего синтезированное никогда не записывается в репозиторий, поэтому каждый последующий читатель — пересоздание при дрейфе настроек, asc app upgrade, дисковое использование, порты, asc app clone — пересинтезирует тот же манифест из способа установки, записанного в метаданных самого приложения, а не падает на отсутствующем asc.yaml.

Установка из голого docker-compose.yml (DMN-108/DMN-109): репозиторий с compose-файлом и без ничего другого ставится как docker compose-проект — ни настроек, ни квоты, ни проверки требований, потому что манифеста тут вообще нет. Это единственное осознанное и задокументированное исключение из правила «Docker только через bollard»: переписывать интерполяцию, env_file, extends, профили, override-файлы и depends_on с ожиданием healthcheck поверх Engine API — отдельный проект работы, поэтому вместо этого вызывается CLI-плагин docker compose (v2), направленный на тот же настроенный сокет, что bollard использует везде. Плагин опционален и пробуется один раз; без него compose остаётся способом «найдено, не устанавливается», как Swarm/Kubernetes. Установка провижинит через docker compose create (контейнеры существуют, но не запущены — тот же контракт, что у обычного Docker-приложения); «стоп» — это stop, никогда не down, поэтому контейнеры и тома приложения переживают его; порты читаются из compose-файла, поэтому остановленное приложение всё равно их сообщает; exec/attach отклоняются (нет одного контейнера для адресации), а логи работают, уже мультиплексированные по сервисам. Самая важная проверка — отказ любому сервису, который монтирует хостовый путь за пределами склонированного репозитория, ещё до создания хоть одного контейнера.

Реестры ​

  • Формат реестра (repo registry) — иерархия JSON-файлов: корневой индекс registry.json → файлы категорий categories/<тема>.json (databases, ai, bots, game-servers, system-utilities, web…) → опциональные подкатегории (children). Пакеты бывают двух типов: app (asc.yaml) и stack (asc.stack.yaml). Схемы валидации — в registry/schema/. Описания — на английском.
  • Дерево источников: sourcelist демона → из всех реестров строится дерево (по ссылкам index/children корневого индекса), затем объединённый список приложений (для каждого пользователя); в результатах поиска конфликт имён решает приоритет источника.
  • Конфликт имён при установке: если пакет предоставляют несколько источников, asc install в терминале выводит список кандидатов (имя источника + репозиторий) и спрашивает, какой использовать (выбор номера); неинтерактивные вызовы получают ошибку с тем же списком — реестр указывается явно: asc install <пакет> --source <имя> (API: поле source в InstallAppRequest). asc app upgrade предпочитает источник, из которого приложение было установлено.
  • Типы источников: file:// (локальный каталог) и https:// (реестр, GitHub raw).
  • Устойчивость загрузки (DMN-036): каждый файл индекса реестра (registry.json, файлы категорий) — отдельный вызов curl без переиспользования соединения между ними, то есть весь asc update/asc search — это короткая серия из нескольких мелких HTTPS-запросов к одному хосту. Раньше зависшее соединение на любом из них (throttling CDN, временный сетевой сбой) вешало всю команду на срок до 5 минут без единого вывода. Теперь запросы к реестру используют короткий таймаут на запрос (20с) и пару ретраев на транзиентные ошибки — вместо щедрого бюджета в 300с, зарезервированного для больших загрузок (релизные файлы asc-updater).
  • Прогресс asc update (DMN-037): в терминале на каждый файл индекса выводится своя строка прогресса, в стиле docker pull/git clone — спиннер, пока запрос выполняется, и заморозка на размере в байтах при успехе или на тексте ошибки при провале. Поскольку реестр забирается по одному мелкому файлу за раз, это превращает застрявшую загрузку в видимо застрявший спиннер вместо молчаливого ожидания.
  • Source list по пользователям: два уровня списков —
    • системный /etc/asc/sources.toml — правит root (sudo asc source add|remove), источники видны всем пользователям сервера;
    • пользовательский ~/.config/asc/sources.toml — каждый пользователь ведёт свой список (asc source add|remove без sudo), он дополняет системный.
    • Эффективный список = системные источники (приоритетнее) + свои; затенять и удалять системные источники пользователь не может (asc source list показывает происхождение каждого источника). Кэш индексов у root — в data_dir, у пользователя — в ~/.cache/asc/.
  • Политика установки ([policy] в /etc/asc/config.toml, правит root): user_install = "all" (по умолчанию — пользователи ставят любые пакеты: Docker, нативные, утилиты) или user_install = "docker" (пользователям — только Docker-приложения; нативные и утилиты ставит только root). Применяется при asc install; на root не действует.
  • Кэш индексов с TTL + asc update для принудительного обновления.
  • CLI: asc install|remove|upgrade <pkg> (или asc install <git-url> [--branch|--tag] [--path <подкаталог>] [--app <приложение стека>] [--force]), asc search <query>, asc source add|remove|list, asc update.

🔗 Связанные задачи ​

DMN-003, DMN-018, DMN-038, DMN-040, DMN-045, DMN-046, DMN-047, DMN-048, DMN-059, DMN-084, DMN-096, DMN-097, DMN-098, DMN-099, REG-001, REG-002, REG-005, BE-002, BE-003, BE-028, BE-029, BE-030, NODE-031, NODE-032, NODE-033, FE-090, FE-091 в ROADMAP.md; GRW-011 в ROADMAP-GROWTH.md.

Распространяется по лицензии MIT.