📦 Пакетный менеджер и реестры
📌 Описание
Пакетный менеджер в духе 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 (черновик)
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 и собирать образ локально движком вместо (или наряду с) скачиванием готового:
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-режиме берутся дефолты):
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 → применение при (пере)запуске, без отдельного канала доставки.
{ "$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):
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}:
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: манифеста:
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.
Механика установки: клонирование репозитория
Установка приложения = клонирование его репозитория:
asc install <package>→ демон клонирует репозиторий пакета в/asc/apps/<id>/repository/.- Версии приложения = 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, без установки/клонирования: им пользуется выбор версии на платформе (мастер установки, выпадающий список при обновлении). - Согласие с лицензией (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 платформы в обоих случаях отрисует свой диалог согласия из этих полей. Для стека вопрос задаётся один раз на репозиторий. Пакеты без файла лицензии ставятся без вопроса. - Пользовательское название и несколько инстансов (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продолжает резолвить его корректно. - Имена нечувствительны к регистру, 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попадают в одно и то же приложение. Репозиторий, который ставится только после переименования автором, — это баг, а не правило. - Дальше демон работает с локальной копией: читает
asc.yaml/asc.settings.yaml, собирает/запускает по типу приложения. - Для Docker-приложений контейнер создаётся через Engine API; отсутствующий на хосте образ скачивается автоматически из его реестра (
runtime.image; имя без тега означаетlatest) — и при установке, и при обновлении. - Прогресс-бары: в терминале клонирование репозитория (
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).
{
"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_COMMANDc-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:
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.