# AppRafter — План разработки > **Источник истины:** [`spec.md`](./spec.md) (revision 4). > **Назначение:** разбить spec на упорядоченные actionable-фазы, каждая из которых пригодна как самостоятельный цикл «план → реализация». > **Статус:** живой документ. Обновляется по мере закрытия фаз и появления новых решений. --- ## 0. Как пользоваться этим документом 1. **Цикл работы:** выбираем одну подфазу (например, `1.4`), раскрываем её через skill `superpowers:writing-plans` в детальный TDD-план в `docs/superpowers/plans/YYYY-MM-DD-.md`, исполняем через `subagent-driven-development` или `executing-plans`, отмечаем чекбоксы здесь. 2. **Гранулярность:** каждая подфаза — это **один цикл разработки** (~1–5 рабочих дней). Если оказывается больше — делим на лету. 3. **Зависимости:** идти по фазам сверху вниз. Внутри фазы соблюдать указанные `Зависит от:`. Параллелить можно ветки без общих зависимостей. 4. **Размер (T-shirt):** `XS` ≈ полдня, `S` ≈ 1–2 дня, `M` ≈ 3–5 дней, `L` ≈ ≥1 неделя (кандидат на дробление). 5. **Acceptance:** перед закрытием подфазы должны выполниться все её критерии приёмки. Без этого галка не ставится. 6. **Критерий «готовности к запуску цикла»:** spec-ссылка ясна, зависимости закрыты, acceptance проверяемы. Если что-то размыто — сначала ADR в `docs/adr/`. ### Условные обозначения - `[ ]` — не начато - `[~]` — в работе (с пометкой ветки/PR в скобках) - `[x]` — закрыто (с пометкой даты и коммита) - `🔒` — заблокировано (с указанием блокера) - `⚡` — критический путь - `🌱` — можно запараллелить - `> 🏁 SR:` — speedrun bucket: **A** keep (launch) · **B** pull-up (launch) · **C** defer post-launch (+ trigger) · **D** drop (+ reactivate). "order N" = OSS-core build sequence (speedrun §4.2). See `speedrun-plan.md`. --- ## 1. Карта фаз | Фаза | Название | Соответствие spec | Размер | Зависит от | |------|----------|-------------------|--------|------------| | 0 | Основания и подготовка | M0 finalization | M | — | | 1 | MVP single-node | M1 | L | 0 | | 1.9 | Dev Mode MVP (1B) | dev-mode-task.md §20 Phase 1B | M+ | 1.5 | | 2 | Платформенные сервисы | M2 | L | 1 | | 2.9 | Dev Mode + Services (2B) | dev-mode-task.md §20 Phase 2B | M | 1.9, 2 | | 3 | Multi-node + observability | M3 | L | 2 | | 3.9 | Dev Mode Full (3B) | dev-mode-task.md §20 Phase 3B | M | 2.9, 3 | | 4 | External Surface + Access | M4 | L | 3 | | 5 | Tier 3 — bare metal | M5 | L | 4 | | 6 | Tier 4 — confidential | M6 | M | 5 | | 7 | Plugin ecosystem | (cross-cut) | L | 2 (gRPC), 3 (infra) | | 8 | 1.0 release | M7 | M | 4 (минимально), идеально 6 | | ∞ | Сквозные направления | — | — | running | Phase 7 запускается параллельно с 3+ как только готов CRD ServiceProvider (закроется в фазе 2). --- ## Фаза 0 — Основания и подготовка ⚡ **Цель фазы:** превратить repo из черновика spec в готовую к контрибьюторам монорепу с зафиксированными решениями M0. **Spec:** §6 (M0), §7 (Resolved → license, codename), Appendix A. ### 0.1 Структура монорепы 🌱 **Статус:** ✅ закрыто 2026-05-06. **Цель:** создать каркас директорий по Appendix A с README-заглушками. **Поставка:** - [x] Создать каталоги: `cli/`, `operator/`, `schemas/`, `providers/{pg-integrated,pg-aws,jetstream-integrated,clickhouse-integrated,redis-integrated,s3-integrated}/`, `backstage-plugins/`, `manifests/`, `docs/`, `examples/`, `docs/adr/`, `docs/superpowers/plans/`. - [x] В каждом каталоге — README.md с одним абзацем «что здесь». - [x] Корневой `README.md` с vision, схемой, ссылкой на `spec.md` и `plan.md`. - [x] `.editorconfig`, `.gitattributes`, базовый `.gitignore` (Rust, Node, Bun, OS-артефакты). **Acceptance:** `tree -L 2` соответствует Appendix A; README рендерится в GitHub-flavoured markdown. **Размер:** XS --- ### 0.2 Лицензия FSL-1.1-MIT 🌱 **Статус:** ✅ закрыто 2026-05-06. **Цель:** оформить лицензионное решение из §7. **Поставка:** - [x] `LICENSE` в корне с текстом FSL-1.1-MIT (канонический шаблон с fsl.software, copyright «AppRafter Authors», year 2026). - [x] `LICENSE-MIT` для будущей конверсии (для прозрачности). - [x] `NOTICE` с описанием модели (2-летнее окно → MIT). Только английский — публичный документ (см. правило о языке проекта). - [x] SPDX-заголовок-шаблон в `docs/contributing/license-headers.md`. - [x] Подпапки плагинов (`providers/`, `backstage-plugins/`) — отдельный `LICENSE` MIT (см. §7). **Acceptance:** GitHub распознаёт лицензию (текст канонический FSL-1.1-MIT с fsl.software — Linguist распознаёт по характерному тексту); SPDX-header задокументирован для всех будущих исходников. **Размер:** XS --- ### 0.3 ADR-процесс и шаблон **Статус:** ✅ закрыто 2026-05-06. **Цель:** зафиксировать формат принятия архитектурных решений. **Поставка:** - [x] `docs/adr/0000-template.md` (по шаблону Майкла Найгарда + risk/owner/re-evaluation). - [x] `docs/adr/0001-license-fsl-1-1-mit.md` — FSL-1.1-MIT для core, MIT для плагинов (§7 + §8). - [x] `docs/adr/0002-codename-apprafter.md` — выбор кодового имени (§7 open-question 9). - [x] `docs/adr/0003-rust-operator-over-crossplane.md` (§8). - [x] `docs/adr/0004-cue-over-pkl.md` (§7 + точка пересмотра M5). - [x] `docs/adr/0005-kine-nats-over-etcd.md` (§4.2 + §8). - [x] `docs/adr/0006-openbao-over-vault.md` (§4.4 + §8). - [x] `docs/adr/0007-tier-1-sealedsecrets-tier-2-openbao.md` — переназначено с дублирующего FSL-обоснования на принцип 1.8 (§1.8 + §4.4 + §8). - [x] `docs/adr/0008-http-first-notifications-api.md` (§4.6 + §8). - [x] `docs/adr/0009-platform-only-templates.md` (§4.6 + §8). - [x] `docs/adr/0010-dockerfile-first-build.md` (§4.9 + §8). - [x] `docs/adr/0011-hybrid-rust-sdk-tofu-shim.md` (§3.7 + §4.12 + §8). - [x] `docs/adr/0012-migrationplan-as-first-class.md` (§3.8 + §8). - [x] `docs/adr/README.md` обновлён индексом всех ADR. **Acceptance:** все «Resolved»-решения §7 и тех-обоснования §8 закодифицированы как ADR; индекс соответствует фактическому содержимому каталога. **Размер:** S --- ### 0.4 CUE-модуль и валидация **Статус:** ✅ закрыто 2026-05-06. **Цель:** инициализировать единый CUE-модуль для всех схем платформы. **Решения по ходу:** - `cue.mod/` положен **в корень репо**, не в `schemas/` — стандартная CUE-практика для monorepo (один модуль, schemas + examples в нём). - Имя модуля — `apprafter.io` (вместо `github.com/apprafter/schemas`); короче, согласовано с `apiVersion: apprafter.io/v1alpha1`. - Каркас 9 CRD — skeleton с минимальным набором полей. Полные production-grade схемы (`Application` с env-overrides, `ServiceProvider` с tier-defaults, и т.д.) докручиваются в фазах 1.7 / 2.1 / 2.2 / 4.1 / 4.5 / 4.16 / 5.x. - `schemas/k8s/` пока пустой каталог с README — импорт upstream Kubernetes типов через `cue import` подключается в фазе 1.7, когда renderer операторa получит конкретные `Deployment`/`Service`/Gateway типы. **Поставка:** - [x] `cue.mod/module.cue` (`module: "apprafter.io"`, language v0.10.0). - [x] `schemas/k8s/` — placeholder с README; импорт отложен до фазы 1.7. - [x] `schemas/v1alpha1/` — skeleton всех 9 CRD: `Application`, `ServiceProvider`, `ResourceClaim`, `AccessGrant`, `MigrationPlan`, `ExternalSurface`, `Infrastructure`, `ServiceProviderPlugin`, `InfrastructureProviderPlugin`, плюс общий `types.cue`. - [x] `scripts/lint-cue.sh` — `cue fmt --check` + `cue vet` для schemas и examples; fallback на `nix run nixpkgs#cue` если нет локального бинарника. - [x] `examples/applications/parser.cue` — валидная фикстура (упрощённая версия §3.1). **Acceptance:** - ✅ `scripts/lint-cue.sh` зелёный (CUE 0.16.0 через `nix run`). - ✅ Невалидный пример (`replicas: "three"`, `port: "not-a-port"`, `public: "yes"`) валится с понятными сообщениями `conflicting values and (mismatched types ...)` со ссылками на line:column в schema и в example. **Размер:** M --- ### 0.5 Bootstrap CI 🌱 **Статус:** ✅ закрыто 2026-05-06. **Цель:** GitHub Actions / CI-пайплайн с минимальным набором проверок. **Решения по ходу:** - Lefthook config назван `lefthook.yml` (без leading dot — стандартный путь, который lefthook ищет по умолчанию). - Rust- и Bun-job'ы условные: пока в репе нет ни `Cargo.toml`, ни `package.json`, оба пропускают шаги с `::notice`. Реальная проверка включится в фазе 1.1 (cli) и 1.6 (Backstage). - SPDX-чек реализован через `scripts/check-spdx-headers.sh` — `git ls-files` против явного списка `PATTERNS`. Markdown-доки и сгенерированные файлы исключены. - Conventional-commits enforce'ится в двух местах: PR-title через GitHub Action `amannn/action-semantic-pull-request@v5`, локальный commit-msg — через `scripts/check-commit-msg.sh` (привязан в `lefthook.yml`). **Поставка:** - [x] `.github/workflows/lint.yml` — три job'а: CUE (`./scripts/lint-cue.sh`), Rust (`cargo fmt --check` + `cargo clippy -D warnings`, conditional), Bun (`bun lint`, conditional). - [x] `.github/workflows/test.yml` — Rust (`cargo test`) + Bun (`bun test`), оба conditional. - [x] `.github/workflows/license-check.yml` — `./scripts/check-spdx-headers.sh`. - [x] `.github/workflows/conventional-commits.yml` — PR-title проверка. - [x] `.github/CODEOWNERS` (с placeholder-handle `@apprafter-authors`). - [x] `.github/PULL_REQUEST_TEMPLATE.md`. - [x] `.github/ISSUE_TEMPLATE/{bug,feature,adr-proposal}.yml`. - [x] `lefthook.yml` — pre-commit (`lint-cue.sh`, `check-spdx-headers.sh`) и commit-msg (`check-commit-msg.sh`). - [x] `scripts/check-spdx-headers.sh` — проходит на всех 25 текущих source-файлах. - [x] `scripts/check-commit-msg.sh` — Conventional Commits validator (тот же набор типов, что и в CI). **Acceptance:** - ✅ `scripts/check-spdx-headers.sh` зелёный для всех 25 tracked source-файлов; добавление файла без SPDX → fail с `::error file=...::missing SPDX-License-Identifier`. - ✅ `scripts/check-commit-msg.sh` принимает `feat(repo): ...`, отвергает «random non-conventional message» с понятным сообщением. - ✅ `scripts/lint-cue.sh` (вызывается lint workflow) продолжает быть зелёным. **Размер:** S --- ### 0.6 DevContainer / dev-окружение 🌱 **Статус:** ✅ закрыто 2026-05-06. **Цель:** контрибьютор клонирует repo и в один шаг получает всё нужное. **Решения по ходу:** - Три параллельных install-пути: Nix flake (рекомендуемый), VS Code Dev Container (postCreate скачивает CUE/k3d/just/lefthook/cosign), и manual через `mise.toml` для language runtimes + ручная установка остальных. Все три ведут к одному `just bootstrap && just e2e-up`. - `Justfile` вместо Makefile — современный синтаксис, рекурсивные shebang-блоки для условных шагов. - `flake.lock` закоммичен — pinning nixpkgs revision для воспроизводимости. **Поставка:** - [x] `.devcontainer/devcontainer.json` (Rust + Node + Bun + Go + kubectl + helm + docker-in-docker через features) и `.devcontainer/post-create.sh` (CUE, k3d, just, lefthook, cosign). - [x] `flake.nix` — полный devShell: cue, cargo+rustc+rustfmt+clippy+rust-analyzer, bun, kubectl, k9s, helm, k3d, argocd, cilium-cli, talosctl, cosign, syft, trivy, grype, just, lefthook, age, sops, jq, git. `nix flake check` — зелёный. - [x] `flake.lock` — пин nixpkgs (rev 549bd84d…) и flake-utils. - [x] `mise.toml` — rust/bun/node/just/go (language runtimes; для остальных тулз ссылка на Nix flake / dev container). - [x] `Justfile` — 8 таргетов: `default`, `bootstrap`, `lint`, `fmt`, `test`, `e2e-up`, `e2e-down`, `stats`. `just --list` рендерит дерево. - [x] `docs/contributing/setup.md` — три install-пути, bootstrap, e2e-up/-down, common issues. - [x] `docs/contributing/README.md` — индекс contributor-документов. - [x] Корневой `README.md` дополнен секцией Quick Start. **Acceptance:** - ✅ `nix flake check --no-build` — зелёный (devShell + formatter эвалюируются). - ✅ `nix run nixpkgs#just -- --justfile Justfile --list` — выводит все 8 рецептов. - ✅ Контрибьютор: `git clone` → `nix develop` (или Dev Container reopen) → `just bootstrap && just e2e-up` без чтения дополнительных доков (Quick Start в корневом README покрывает базовый flow). **Размер:** S --- ### 0.7 Базовый docs-skeleton **Статус:** ✅ закрыто 2026-05-06. **Цель:** заготовка под TechDocs (M7), сейчас — навигационный каркас. **Решения по ходу:** - `docs/README.md` (Phase 0.1) удалён — конфликтовал с `docs/index.md` и блокировал mkdocs strict mode. Содержимое перенесено в `docs/index.md` (mkdocs landing page). - mkdocs `exclude_docs` исключает `superpowers/` (локальный gitignored каталог, который физически виден mkdocs на диске). - `validation.nav.omitted_files: info` — ADR-страницы доступны по URL, но не дублируются в боковом nav (одной ссылкой «ADRs» → `adr/README.md` достаточно). - mkdocs-material в `flake.nix` (вместе с базовым mkdocs) — `nix develop` сразу даёт `mkdocs serve/build`. - В `Justfile` добавлены `docs-serve` (live preview) и `docs-build` (strict). **Поставка:** - [x] `docs/index.md` — landing page с tier-таблицей и ссылками на разделы. - [x] `docs/architecture/index.md` — stub, ссылки на §2/§4 spec.md. - [x] `docs/concepts/index.md` — stub, таблица §3-объектов и порядок чтения. - [x] `docs/operator-guide/index.md` — stub. - [x] `docs/dev-guide/index.md` — stub. - [x] `docs/reference/index.md` — stub. - [x] `mkdocs.yml` с Material theme, plugins (search), pymdownx-расширениями и валидной nav. - [x] `CONTRIBUTING.md` (root) — entry point для новых контрибьюторов. - [x] `CODE_OF_CONDUCT.md` — Contributor Covenant 2.1. - [x] `SECURITY.md` — disclosure policy. - [x] `GOVERNANCE.md` — роли и decision-making (lazy consensus + ADR-process). - [x] `Justfile` — таргеты `docs-serve`, `docs-build`. - [x] `flake.nix` дополнен `python3Packages.mkdocs-material`. - [x] `.gitignore` дополнен `site/`. **Acceptance:** - ✅ `mkdocs build --strict` (через `nix-shell -p (python3.withPackages [mkdocs mkdocs-material])`) — зелёный, 0 warnings, build за 0.39 s. - ✅ Навигация согласована со spec: Architecture/Concepts/Operator/Dev/Reference + Contributing + ADRs соответствуют §2/§3/§4/§7/§8. **Размер:** S --- ### 0.8 Закрытие чек-листа M0 spec **Статус:** ✅ закрыто 2026-05-06. **Цель:** обновить `spec.md` §6 (M0): зачеркнуть «Repository structure defined» и «License chosen». **Решения по ходу:** - Версия не `v0.0.0-foundations` (как было в первоначальной редакции плана), а `v0.0.8` — в соответствии с патч-нумерацией, которой мы ведём всю Phase 0 (по решению пользователя «начнём с 0.0.1 и пойдём по патч-версиям»). - License-комментарий в spec.md переписан с «candidates: MPL-2.0, Apache-2.0» на «FSL-1.1-MIT for core, MIT for plugins; see ADR 0001» — фактическое решение. **Поставка:** - [x] `spec.md` §6 M0 — оба оставшихся пункта переведены в `[x]`. - [x] `docs/changelog/history.md` — Keep a Changelog v1.1 формат, секция Phase 0 (v0.0.1 → v0.0.8) с Added/Changed. **Acceptance:** - ✅ spec.md M0 полностью закрыт. - ✅ Tag `v0.0.8` (заменяет упомянутый в исходном плане `v0.0.0-foundations`). **Размер:** XS --- ## Фаза 1 — MVP single-node (M1) ✅ **Цель фазы:** на чистом Hetzner CX22 за один `platform-cli init` поднять Tier 1 кластер и задеплоить hello-world `Application` через GitOps. **Spec:** §6 M1, §4.1 (Tier 1), §4.5, §4.12, §3.1. ### 1.1 platform-cli — каркас CLI **Статус:** ✅ закрыто 2026-05-06. **Цель:** Rust-бинарник `platform-cli` с командами-заглушками `init|plan|apply|status|login|upgrade-tier`. **Решения по ходу:** - Версионная схема Phase 1: `0.1.x` (минор = фаза, патч = подфаза). - State хранится как JSON (`.apprafter/state.json`), не CUE-encoded. Переход на CUE-encoded — позже, когда схема состояния стабилизируется. - CUE-доступ через subprocess (`cue export ... --out json`); FFI-вариант (`cuelang-go`) отложен. - Workspace из четырёх крейтов: `platform-cli` (бинарь), `cli-core` (ошибки + Tier + логи + CUE), `cli-state` (state-файл), `cli-providers` (трейт + `DryRunProvider`). - `cue::export_in(workdir, path)` добавлен в API: `cue` отказывается от абсолютных путей и требует относительный путь от module-root, поэтому wrapper вызывает `cue` с `current_dir(workdir)`. Простой `export(path)` — обёртка над `export_in(cwd, path)`. - Все команды печатают «would …» с указанием будущей фазы plan.md, в которой стаб станет реальной операцией. **Поставка:** - [x] Cargo workspace `cli/` с тремя крейтами + бинарь (`platform-cli`, `cli-core`, `cli-state`, `cli-providers`). - [x] CUE-доступ через subprocess (`cli-core::cue::{export, export_in}`), `CUE_BIN` env-override. - [x] Структурированный логгер (`tracing` + `tracing-subscriber` с `EnvFilter`). - [x] State-файл `.apprafter/state.json` (JSON в skeleton-фазе) с `load_or_default` / `save`. - [x] Шесть команд-стабов через clap derive API. **Acceptance:** - ✅ `platform-cli --help` показывает все шесть subcommand'ов. - ✅ `platform-cli plan` на пустом state выдаёт `no changes`. - ✅ Workspace проходит `cargo fmt --all -- --check`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`. **Зависит от:** 0.4 **Размер:** M --- ### 1.2 Hetzner Cloud built-in provider **Статус:** ✅ shipped — sub-phase 1.2 полностью закрыта серией циклов `v0.1.2`–`v0.1.7`, покрывающих server / SSH-keys / network / firewall / CUE-parsing / floating IP / state import. **Цель:** нативный provider в `platform-cli` через прямую интеграцию с Hetzner Cloud REST API (через `ureq`, blocking). **Решения по ходу:** - Используем `ureq` (blocking) + ручные wire-types вместо third-party `hcloud`-crate'ов: тоньше, без async, без неактивных внешних зависимостей. - Mock-тесты через `mockito` 1.x (sync HTTP). - Метим все managed-ресурсы лейблом `apprafter=true` — идемпотентность и будущий `import` строятся вокруг этого фильтра. - `Provider` trait расширен: добавлен `destroy()`, `Plan.changes` переименован в `Plan.actions: Vec` (типизированные `CreateServer` / `DestroyServer` / `Noop`). - `State` дополнен `hetzner_cloud: Option` (server_id + server_name). - Server boot пока без SSH-ключей (Hetzner возвращает root-password). SSH-keys приедут вместе с network/firewall в следующем цикле. **Поставка (server-CRUD ветка, v0.1.2):** - [x] `cli-providers/hetzner_cloud/`: серверный CRUD (list / create / delete) с mockito-тестами. - [x] `Provider::destroy()` + типизированный `Action`. - [x] `HetznerCloudClient` (list / create / delete server) с idempotent 404-on-delete. - [x] `HetznerCloudProvider` impl `Provider`: refresh + diff + apply + destroy. - [x] CLI `destroy --yes` команда + дисциплина `apply` (требует `HCLOUD_TOKEN`, без него — внятная ошибка). - [x] `examples/infrastructure/tier-1-hetzner.cue` фикстура. - [x] `#[ignore]`-тагнутый e2e-тест против реального Hetzner. **Поставка (SSH-keys, v0.1.3):** - [x] `HetznerCloudClient` методы для SSH-keys: list / create / delete. - [x] `Action::CreateSshKey` / `DestroySshKey`, `SshKeySpec`. - [x] `ServerCreateRequest.ssh_keys` (`Option>`, serde-skip). - [x] `HetznerCloudProvider.ssh_keys: Vec` — refresh + idempotent create + ordered apply (ssh-keys → server) + ordered destroy (server → ssh-keys). - [x] `HetznerCloudState.ssh_key_ids` cache (back-compat через `#[serde(default)]`). - [x] `apply` читает `APPRAFTER_SSH_PUBLIC_KEY` env: при наличии — boot с SSH-key (без root-password). **Поставка (Network + Firewall, v0.1.4):** - [x] `HetznerCloudClient` методы для Network: list / create / delete. - [x] `HetznerCloudClient` методы для Firewall: list / create / delete. - [x] `ServerCreateRequest.networks` + `ServerCreateRequest.firewalls` (Option, serde-skip). - [x] `Action::CreateNetwork` / `DestroyNetwork` / `CreateFirewall` / `DestroyFirewall`. - [x] `NetworkSpec`, `FirewallSpec`, `FirewallRuleSpec`. - [x] `HetznerCloudProvider.{networks,firewalls}` — ordered apply (ssh → net → fw → server) и destroy (server → fw → net → ssh). - [x] `HetznerCloudState.{network_id,firewall_id}` cache. - [x] CLI `apply` строит дефолтные `NetworkSpec` (10.0.0.0/16 + 10.0.0.0/24 в `eu-central`) и `FirewallSpec` (SSH 22 + HTTPS 443 ingress) из имени кластера. **Поставка (CUE Infrastructure parsing, v0.1.5):** - [x] CUE schema `Infrastructure` расширена optional полями (`region`, `network` с `subnet`, `firewall.ingress`, `sshKeys`, `osImage`). - [x] `examples/infrastructure/tier-1-hetzner.cue` — полный пример (network 10.0.0.0/16 + subnet eu-central + SSH/HTTPS ingress + osImage). - [x] `cli-core::manifest` модуль — `InfrastructureManifest` типы и `parse_infrastructure(workdir, path)` через `cue::export_in`. - [x] `apply.rs`: при `APPRAFTER_MANIFEST=` читает manifest и накладывает на v0.1.4-дефолты (server_type, image, network/subnet/zone, firewall rules, ssh keys). Без env var поведение v0.1.4 не изменилось. **Поставка (Floating IP, v0.1.6):** - [x] `HetznerCloudClient` методы для Floating IPs: list / create / delete (404-idempotent). - [x] Wire-types `FloatingIp`, `FloatingIpListResponse`, `FloatingIpCreateRequest`/`Response`, `HomeLocation`. - [x] `Action::CreateFloatingIp` / `DestroyFloatingIp`, `FloatingIpSpec`. - [x] `HetznerCloudProvider.floating_ips` — refresh + idempotent create + ordered apply (ssh → net → fw → server → fip с `server` атрибутом сразу при создании) + ordered destroy (fip → server → fw → net → ssh). - [x] `HetznerCloudState.floating_ip_ids` cache (back-compat через `#[serde(default)]`). - [x] CUE schema: `network.floatingIPs: [...string]` (оставалось зарезервированным с v0.1.5). - [x] CLI `apply` читает `network.floatingIPs` из manifest, префиксует имена кластером и передаёт как `FloatingIpSpec` (`ipv4`, `home_location = region`). - [x] `examples/infrastructure/tier-1-hetzner.cue` — `floatingIPs: ["egress"]`. **Поставка (`platform-cli import`, v0.1.7):** - [x] `Commands::Import { force, dry_run }` clap-вариант + dispatch в `main.rs`. - [x] `commands::hcloud::hcloud_base_url()` — общий хелпер для `apply`/`destroy`/`import`, читает `APPRAFTER_HCLOUD_BASE_URL` (test-only) с фолбэком на `DEFAULT_BASE_URL`. - [x] `commands::import::run` — read-only сканирование `apprafter=true` ресурсов, сборка `HetznerCloudState` по `cluster_name`, флаги `--dry-run` (печатает summary, не пишет state) и `--force` (перезаписывает существующий `state.hetzner_cloud`). - [x] Integration-тесты на `assert_cmd` + `mockito`: happy-path с записью state, `--dry-run` без записи, "no matching server" → friendly message, фильтр по `apprafter` лейблу, `--force` overwrite-guard. - [x] `cli/README.md` — новая секция "Recovering state with `import`". **Полное закрытие 1.2:** - [x] `plan.md` отражает все 6 циклов как ✅; sub-phase 1.2 переведён из 🚧 partial в ✅ shipped. **Acceptance (v0.1.2):** - ✅ `platform-cli apply` (с валидным state + `HCLOUD_TOKEN`) поднимает 1× CX22. - ✅ Повторный `platform-cli apply` — no-op (refresh видит сервер, Plan = пустой). - ✅ `platform-cli destroy --yes` сносит сервер и чистит state. - ✅ Mocked-тесты (12 шт.) + `#[ignore]` integration test компилируются и запускаются вручную с реальным Hetzner. **Зависит от:** 1.1 **Размер:** L (разбит на 6 циклов: server-CRUD `v0.1.2` ✅, SSH-keys `v0.1.3` ✅, network+firewall `v0.1.4` ✅, CUE-parsing `v0.1.5` ✅, floatingIP `v0.1.6` ✅, import `v0.1.7` ✅) --- ### 1.2 AUDIT — Hetzner Cloud built-in provider: IPv6 support ✅ > v0.1.70 — 1.2 AUDIT shipped (partial): wire-type IPv6 parsing, k3s dual-stack cluster/service CIDRs, Hetzner Firewall ICMP allow-rule. `--node-ip` dual-binding и реальный pod-level dual-stack smoke прицеплены к зависимым подфазам (3.1 HA-bootstrap пересекает `--node-ip`; 1.4 AUDIT закрывает Cilium values для pod connectivity). **Source:** ADR 0017. **Поставка:** - [x] `cli-providers/src/hetzner_cloud/types.rs` — новый `PublicIpv6 { ip: String }` (хранит `::/64` CIDR-строку как Hetzner возвращает) + `PublicNet.ipv6: Option`. Два regression-guard теста в `tests/types_test.rs` пинят deserialize sample response (dual-stack + только-v6 forward-compat ветка). Re-export `PublicIpv6` через `cli-providers::hetzner_cloud`. - [x] **Hetzner private network остаётся IPv4-only** (фундаментальное ограничение Hetzner — public IPv6 идёт через server's public interface, private network — внутрикластерная IPv4). Без изменений в `NetworkSpec`; ADR 0017 это явно признаёт. - [x] `cli-providers/src/hetzner_cloud/user_data.rs` — `K3sBootstrapOptions { dual_stack: bool }` (`Default::default()` = `true`), `build_k3s_user_data` теперь добавляет `--cluster-cidr=10.42.0.0/16,fd00:42::/64 --service-cidr=10.43.0.0/16,fd00:43::/112` per ADR 0017. Константы `CLUSTER_CIDR_DUAL_STACK` / `SERVICE_CIDR_DUAL_STACK` экспортируются для shared-use. Два regression-guard теста — default install line содержит CIDR-пару, opt-out `dual_stack: false` дропает их без касания других disable-флагов. - [x] `cli/platform-cli/src/commands/apply.rs::default_ingress_rules` — новый ICMP-rule (`direction: in, protocol: icmp, port: None, source_ips: ["0.0.0.0/0", "::/0"]`) per ADR 0017 §Per-tier. Hetzner Cloud Firewall не различает ICMPv4 и ICMPv6 — один `protocol: icmp` правило покрывает обе family. Два regression-guard теста — `default_ingress_rules_emits_one_rule_per_default_port_plus_icmp` (счётчик правил) + `default_ingress_rules_include_icmp_for_pmtu_and_ndp` (shape). - **Отложено:** `--node-ip` dual-binding пока не передаётся — требует cloud-init substitution с runtime-detected IPv4 + IPv6 host addresses (multi-line bash в `runcmd`), что в Tier 1 single-node не блокирует connectivity (k3s auto-detects). Закроется в 3.1 (HA bootstrap), когда heterogeneous-nodes сценарий делает выбор node IP критичным. - **Отложено:** Full e2e dual-stack pod connectivity smoke — зависит от 1.4 AUDIT (Cilium Helm values dual-stack), pod не получит v6 интерфейс без Cilium-конфига. После 1.4 AUDIT добавим pod-level v4+v6 reachability assertion в `e2e/mvp.sh`. **Acceptance:** Hetzner provider парсит IPv6 prefix из API; k3s install line содержит dual-stack CIDR-пару; ICMP allowed в Hetzner Firewall. Pod-level connectivity подтверждается после 1.4 AUDIT (Cilium). **Зависит от:** — **Размер:** M (доставлен как single-cycle audit ~v0.1.70; реальный pod-connectivity smoke выкатим вместе с 1.4 AUDIT) --- ### 1.3 k3s bootstrap на свежем VDS **Статус:** ✅ shipped — sub-phase 1.3 закрыта серией циклов `v0.1.8`–`v0.1.10`: cloud-init bootstrap (k3s + ufw + fail2ban) → kubeconfig retrieval (SSH fetch + URL rewrite) → age-encryption кеша. **Цель:** автоматическая установка k3s в single-node режиме после провижионинга VM. **Поставка (cloud-init bootstrap, v0.1.8):** - [x] `cli-providers::hetzner_cloud::user_data::build_k3s_user_data` — pure builder для `#cloud-config` YAML; собирает install-команду для k3s c `--disable=traefik --disable=servicelb`, ufw default-deny + whitelist (22/6443/80/443 tcp + 51820 udp), fail2ban для SSH jail. - [x] `ServerCreateRequest.user_data: Option` (serde-skip when None) + `ServerSpec.user_data` + проброс через `HetznerCloudProvider::create_request`. - [x] CLI `apply` ставит `user_data = Some(build_k3s_user_data(...))`; default Hetzner-firewall расширен до tier-1 whitelist (тот же набор портов, что и в ufw). **Поставка (kubeconfig retrieval, v0.1.9):** - [x] `Server.public_net.ipv4.ip` wire field — `cli-providers::hetzner_cloud::types` теперь декодирует public IPv4 с list-ответа. - [x] `cli-providers::hetzner_cloud::kubeconfig` — `rewrite_server_url(yaml, public_ip)` + `KubeconfigFetcher` trait + `SshKubeconfigFetcher` impl (shell out на системный `ssh`, BatchMode/StrictHostKeyChecking=accept-new); `default_ssh_identity_path()` читает `APPRAFTER_SSH_PRIVATE_KEY` с фолбэком на `~/.ssh/id_ed25519`. - [x] `HetznerCloudState.kubeconfig_yaml: Option` (`#[serde(default)]`). - [x] `Commands::Kubeconfig { refresh }` + `commands::kubeconfig::run` + `compute_kubeconfig` orchestrator (cached / cold-fetch / `--refresh`). - [x] Unit-тесты на `rewrite_server_url`, argv-shape `SshKubeconfigFetcher`, `compute_kubeconfig` через `FakeFetcher` (cold/cached/--refresh); integration на missing-state error + cached print без SSH. - [ ] (defer to v0.1.10) age-encryption кеша — на этом цикле сохраняем plaintext. **Поставка (age-encryption, v0.1.10):** - [x] `cli-core::secrets` — wrapper над `age` 0.10: `load_or_create_identity` (mode 0600, parent dirs auto), `encrypt_for_recipient` (armored), `decrypt_with_identity`, `default_age_key_path()` (env override + `~/.config/apprafter/age.key` fallback). - [x] `HetznerCloudState.kubeconfig_age: Option` (armored, serde-default); `kubeconfig_yaml` остаётся читаемым один цикл как legacy-fallback и обнуляется на ближайшем `--refresh`. - [x] `commands::kubeconfig::run` шифрует на запись (recipient = .to_public() identity), расшифровывает на чтение, fallback на plaintext-поле. - [x] Integration round-trip через предзаписанный age-blob + `APPRAFTER_AGE_KEY` env override; in-file тесты `cli-core::secrets` (round-trip / wrong-identity / persist+reload / mode-0600 / env override / bech32 sanity). **Acceptance (v0.1.8):** `platform-cli apply` отправляет в Hetzner POST `/v1/servers` с непустым `user_data`-полем; mocked-тесты + unit-тесты builder'а пинят форму YAML. **Acceptance (1.3 целиком, после v0.1.9):** через ~5 минут после `platform-cli init && platform-cli apply` команда `platform-cli kubeconfig | KUBECONFIG=/dev/stdin kubectl get nodes` показывает Ready single node. **Зависит от:** 1.2 **Размер:** M (разбит на 3 цикла: cloud-init `v0.1.8` ✅, kubeconfig retrieval `v0.1.9` ✅, age encryption `v0.1.10` ✅) --- ### 1.4 Cilium + Gateway API установка **Статус:** ✅ shipped — sub-phase 1.4 закрыта серией циклов `v0.1.11`–`v0.1.12`: Cilium через Helm + Gateway API CRDs (1.4a) → default-deny NetworkPolicy + real-cluster smoke (1.4b). **Цель:** заменить flannel на Cilium с kube-proxy replacement и Gateway API. **Поставка (Cilium + Gateway API CRDs, v0.1.11):** - [x] `cli-providers::k8s::cilium_values::cilium_values_yaml()` — pure builder для tier-1 Helm-values (`kubeProxyReplacement: true`, `ipam: kubernetes`, `hubble: enabled: false`, `operator: replicas: 1`). - [x] `cli-providers::k8s::helm` — `HelmRunner` trait + `HelmCli` shell-out + `HelmUpgradeArgs` + `CILIUM_CHART_VERSION = "1.16.5"`. - [x] `cli-providers::k8s::kubectl` — `KubectlRunner` trait + `KubectlCli` shell-out + `ManifestSource` enum + `gateway_api_crds_url()` (pinned `v1.2.1`). - [x] `Commands::ClusterBootstrap` + `commands::cluster_bootstrap::run()` + pure `perform_bootstrap` orchestrator (helm repo add → helm upgrade --install → kubectl apply -f gateway CRDs); driven с fake runners в in-file tests. - [x] `build_k3s_user_data` теперь добавляет `--disable-kube-proxy` к k3s install line — без этого `kubeProxyReplacement: true` бессмыслен. **Поставка (NetworkPolicy + smoke, v0.1.12):** - [x] `cli-providers::k8s::network_policy::default_deny_network_policy_yaml(namespace)` — pure builder для `NetworkPolicy` (apiVersion `networking.k8s.io/v1`, podSelector `{}`, policyTypes ingress + egress, label `apprafter=true`). - [x] `perform_bootstrap` теперь применяет default-deny на `default` namespace после Gateway API CRDs; kube-system намеренно exempt; `kubectl apply -f` идёт из tempfile. - [x] Renamed FakeKubectl test (`perform_bootstrap_runs_helm_repo_then_install_then_two_kubectl_applies`) пинит call sequence + ManifestSource type для каждого apply. - [x] `cli/platform-cli/tests/cluster_smoke_test.rs` — `#[ignore]`-tagged real-cluster smoke; opt-in через `APPRAFTER_K8S_SMOKE=1` + `KUBECONFIG`; проверяет `cilium status --wait`, `kubectl apply --dry-run=server -f Gateway`, наличие default-deny NetworkPolicy. **Acceptance (v0.1.11):** `platform-cli cluster-bootstrap` выводит сводку и завершается успешно (mocked-runner test); реальный smoke (`cilium status` зелёный, `kubectl apply` Gateway проходит admission) — после v0.1.12. **Зависит от:** 1.3 **Размер:** M (разбит на 2 цикла: Cilium + Gateway API CRDs `v0.1.11` ✅, NetworkPolicy + smoke `v0.1.12` ✅) --- ### 1.4 AUDIT — Cilium + Gateway API установка: dual-stack Helm values ✅ > v0.1.71 — 1.4 AUDIT shipped: `cilium_values_yaml()` явно декларирует `ipv4.enabled: true` + `ipv6.enabled: true`; `e2e/mvp.sh` получил Phase 6.4 с pod-level dual-stack assertion (закрывает отложенный 5-й чекбокс 1.2 AUDIT). **Source:** ADR 0017. **Поставка:** - [x] Cilium Helm values builder лежит в `cli-providers/src/k8s/cilium_values.rs` (`cilium_values_yaml()`). - [x] Assess текущего state (v0.1.70): `ipv4.enabled` не объявлен явно (Helm chart 1.16.x default = true, но это implicit); `ipv6.enabled` не объявлен (default = false) → поды никогда не получают v6 интерфейс даже когда k3s выдаёт dual-stack podCIDR. IPAM mode = `kubernetes` (правильно — k3s публикует pod CIDR через Node API, Cilium читает оттуда без собственного allocator'а). - [x] Updated Helm values на dual-stack: добавлены два явных блока `ipv4: { enabled: true }` и `ipv6: { enabled: true }`. IPAM `mode: kubernetes` сохранён без изменений. - [x] Gateway API CRDs install path — verified: `kubectl apply -f gateway-api/standard-install.yaml` ставит **type definitions only**, не listeners. Family-binding происходит при создании Gateway resource'ом (см. позже в 4.1a), и Gateway API spec поддерживает `listener.protocol: HTTPS` без family-restriction — listener bind'ится на любую family, доступную на node (после v0.1.71 — обе). Никаких изменений в install path не требуется. - [x] E2E `Phase 6.4: dual-stack podIPs assertion` в `e2e/mvp.sh` — после Phase 6 (endpoint curl green) делает `kubectl get pod -l app=e2e-hello -o jsonpath='{.items[0].status.podIPs[*].ip}'` и assert'ит наличие **обоих** v4-адреса из `10.42.0.0/16` (k3s podCIDR) и v6-адреса из `fd00:42::/64` (k3s podCIDR v6 + Cilium ipv6.enabled). Без 1.4 AUDIT этот assert валится с понятным сообщением "Cilium ipv6.enabled likely false". Реальный outbound v6 curl-тест (curl -6 ipv6.google.com из pod'а) отложен в Phase 3.x — pod-image `nginxdemos/hello:plain-text` не имеет curl, добавление test-pod с `curlimages/curl:latest --ipv6` пересекается с network observability (Hubble), которую закроет 3.7a. **Acceptance:** `cilium_values_yaml()` декларирует обе family explicit; unit-test `dual_stack_enabled_per_adr_0017` пинит наличие `ipv4:` + `ipv6:` blocks и счётчик `enabled: true` ≥ 2; `e2e/mvp.sh` Phase 6.4 валится если у pod'а отсутствует v6 IP. **Зависит от:** 1.2 AUDIT (Hetzner provider dual-stack) ✅ **Размер:** S — доставлен как single-cycle audit ~v0.1.71. **Known wart (deferred to Track B 1.70):** `helm upgrade cilium` патчит `cilium-config` ConfigMap, но **не** триггерит rotation cilium DaemonSet pods (chart v1.16.x не имеет `checksum/config` аннотации в template'е). На свежий install это не влияет (агенты сразу стартуют с новыми values), но на upgrade существующего кластера оператору приходится вручную `kubectl rollout restart daemonset cilium -n kube-system` + пересоздать pod'ы, чтобы они получили v6 IP. Quick-fix в `cluster-bootstrap` (один `kubectl rollout restart`) добавил бы ~30с к каждому re-run; вместо этого ждём 1.70 (`cluster-bootstrap` rewrite в Argo CD-managed flow), где Argo CD resource hooks решают это нативно — disposable код мы тогда не пишем. --- ### 1.5 Argo CD установка и bootstrap **Статус:** ✅ shipped — sub-phase 1.5 закрыта серией циклов `v0.1.13`–`v0.1.17`: helm install (1.5a) → admin password (1.5b) → cert-manager + ClusterIssuer (1.5c) → Gateway + HTTPRoute (1.5d) → bootstrap-Application + smoke (1.5e). **Цель:** Argo CD как единственный механизм применения манифестов в кластер. **Поставка (Argo CD Helm install, v0.1.13):** - [x] `cli-providers::k8s::argocd_values::argocd_values_yaml()` — pure builder для tier-1 Helm-values (Dex off, Redis-HA off, ApplicationSet on, Notifications off, ClusterIP server, single replicas). - [x] `ARGOCD_CHART_VERSION = "7.7.7"` в том же модуле. - [x] `perform_bootstrap` теперь делает helm repo add `argo` + helm upgrade --install `argocd` после default-deny NP; renamed FakeRunner test пинит call sequence для обеих helm releases (cilium → argocd) и обоих kubectl applies. - [x] `cluster-bootstrap` дропает 4-й tempfile (Argo CD values) рядом с kubeconfig / Cilium values / default-deny NP. **Поставка (admin password retrieval, v0.1.14):** - [x] `KubectlRunner::get_secret_value(name, namespace, key, kubeconfig)` — wraps `kubectl get secret -o jsonpath={.data.}` + base64-decodes; argv-shape unit test. - [x] `Commands::ArgocdPassword { refresh }` + `commands::argocd_password::run` + pure `compute_argocd_password` orchestrator. - [x] `HetznerCloudState.argocd_admin_password_age: Option` (serde-default). - [x] In-file FakeKubectl tests + cli_smoke missing-state error + integration test для cached-path round-trip через `APPRAFTER_AGE_KEY`. **Поставка (cert-manager + self-signed ClusterIssuer, v0.1.15):** - [x] `cli-providers::k8s::cert_manager_values::cert_manager_values_yaml()` — pure builder для tier-1 Helm-values (`installCRDs: true`, single replicas, Prometheus off). - [x] `CERT_MANAGER_CHART_VERSION = "v1.16.2"` в том же модуле. - [x] `cli-providers::k8s::issuer::selfsigned_cluster_issuer_yaml()` — pure builder для `cert-manager.io/v1 ClusterIssuer` `apprafter-selfsigned` (`spec.selfSigned: {}`, label `apprafter=true`); имя issuer'а как `pub const APPRAFTER_SELFSIGNED_ISSUER` чтобы будущие HTTPRoute / Certificate manifests могли ссылаться без магических строк. - [x] `perform_bootstrap` теперь делает helm repo add `jetstack` + helm upgrade --install `cert-manager` после Argo CD, и kubectl apply self-signed issuer; renamed FakeRunner test пинит 3 helm repos / 3 installs / 3 kubectl applies в правильном порядке. - [x] `cluster-bootstrap` дропает 5-й и 6-й tempfile (cert-manager values + selfsigned issuer) рядом с существующими. **Поставка (Gateway + HTTPRoute для Argo CD UI, v0.1.16):** - [x] CUE schema: `spec.argocd.domain?` optional поле в `#Infrastructure`. - [x] Rust manifest mirror: `cli_core::manifest::ArgocdBlock { domain: Option }` + `InfrastructureSpec.argocd: Option`. - [x] `cli-providers::k8s::argocd_gateway::argocd_gateway_yaml(domain)` — pure builder для 3-document манифеста (Gateway + HTTPRoute + Certificate); все ресурсы в namespace `argocd`, label `apprafter=true`, Certificate ссылается на `apprafter-selfsigned` ClusterIssuer. - [x] `perform_bootstrap` подросла `argocd_gateway_path: Option<&Path>` параметром; при Some — kubectl apply после self-signed ClusterIssuer; при None — bootstrap идентичен v0.1.15. - [x] `cluster_bootstrap::run` парсит `APPRAFTER_MANIFEST` если установлен, извлекает domain, conditionally дропает 7-й tempfile. - [x] `examples/infrastructure/tier-1-hetzner.cue` — закомментированный пример opt-in. **Поставка (bootstrap-Application + закрытие 1.5, v0.1.17):** - [x] CUE schema: `spec.argocd.bootstrapRepo?` + `spec.argocd.bootstrapPath?` optional поля. - [x] Rust manifest mirror: `ArgocdBlock.bootstrap_repo: Option` (rename `bootstrapRepo`) + `bootstrap_path: Option` (rename `bootstrapPath`). - [x] `cli-providers::k8s::bootstrap_app::bootstrap_application_yaml(repo_url, path)` — pure builder для `argoproj.io/v1alpha1 Application` `bootstrap` (namespace `argocd`, syncPolicy.automated.prune+selfHeal, label `apprafter=true`); `BOOTSTRAP_APP_DEFAULT_PATH = "."` для пустого пути. - [x] `read_argocd_settings_from_manifest` возвращает struct (domain + bootstrap_repo + bootstrap_path); `cluster_bootstrap::run` conditionally дропает 8-й tempfile. - [x] `perform_bootstrap` подросла `bootstrap_app_path: Option<&Path>` параметром; при Some — kubectl apply после optional Argo CD Gateway. - [x] Real-cluster smoke в `cluster_smoke_test.rs`: `kubectl get application bootstrap -n argocd` под gate `APPRAFTER_BOOTSTRAP_REPO_SMOKE=1`. - [x] Sub-phase 1.5 status: ✅ shipped. **Acceptance (v0.1.13):** `perform_bootstrap` производит `helm install cilium`, `kubectl apply` Gateway CRDs, `kubectl apply` default-deny NP, `helm install argocd` в этом порядке (mocked). Реальный smoke (Argo CD pods Ready, UI reachable, root app sync) — после v0.1.15. **Зависит от:** 1.4 **Размер:** M (разбит на 5 циклов: helm install `v0.1.13` ✅, admin password `v0.1.14` ✅, cert-manager + ClusterIssuer `v0.1.15` ✅, Gateway + HTTPRoute `v0.1.16` ✅, bootstrap-Application + smoke `v0.1.17` ✅) --- ### 1.6 Backstage минимальный деплой **Статус:** ✅ shipped — sub-phase 1.6 закрыта серией циклов `v0.1.18`–`v0.1.20`: k8s-манифесты (1.6a) → app-скаффолд + Dockerfile (1.6b) → app-config ConfigMap + OAuth stub (1.6c). **Цель:** Backstage как pod в кластере, доступный через Gateway. **Поставка (k8s-манифесты + cluster-bootstrap, v0.1.18):** - [x] CUE schema: `spec.backstage.domain?` + `spec.backstage.image?`. - [x] Rust manifest mirror: `BackstageBlock { domain, image }`; `Default` derived. - [x] `cli-providers::k8s::backstage_manifests::backstage_manifests_yaml(domain, image)` — pure builder для 6-document манифеста (Namespace + Deployment + Service + HTTPRoute + Gateway + Certificate); `BACKSTAGE_DEFAULT_IMAGE = "ghcr.io/apprafter/backstage:placeholder"` для пустого image. - [x] `read_argocd_settings_from_manifest` переименован в `read_cluster_settings_from_manifest`, struct расширен backstage-полями. - [x] `perform_bootstrap` подросла `backstage_manifests_path: Option<&Path>` параметром; при Some — kubectl apply после bootstrap-Application. - [x] `manifests/tier-1/backstage/{example.yaml, README.md}` — статический рендеринг builder'а + recipe для refresh через `cargo run --example backstage_example`. **Поставка (app-скаффолд + Dockerfile, v0.1.19):** - [x] `backstage-plugins/host/Dockerfile` — multi-stage Backstage 1.x шаблон (Node 20 builder + slim runtime, копия skeleton/bundle tarballs, EXPOSE 7007, USER node). - [x] `backstage-plugins/host/.dockerignore` — node_modules, build-output, local config, secrets out of context. - [x] `backstage-plugins/host/scripts/scaffold.sh` — обёртка над `npx @backstage/create-app@latest --skip-install` с preflight'ом (Node 20+, target пустой), drop'ом Dockerfile/.dockerignore рядом, печатью next-steps. Shellchecked. - [x] `backstage-plugins/host/README.md` — 6-step workflow scaffold → install → build → push → manifest → cluster-bootstrap; cross-links к Dockerfile, scaffold-скрипту, rendered example manifest, Rust-builder'у. - [x] `cli/README.md` — blockquote-cross-link к host-app README рядом со step 10. **Поставка (app-config ConfigMap + OAuth stub, v0.1.20):** - [x] `cli-providers::k8s::backstage_app_config::backstage_app_config_yaml(domain)` — pure builder для tier-1 `app-config.yaml` (`app.title`, `app.baseUrl`, `backend.baseUrl` + `cors.origin` от `domain`, `backend.listen 0.0.0.0:7007`, `database better-sqlite3 :memory:`, `auth.providers.guest.dangerouslyAllowOutsideDevelopment: true`). - [x] `backstage_manifests_yaml` теперь эмитит 7-document YAML — добавлен `ConfigMap` `backstage-config` с `data["app-config.yaml"]: |`, и Deployment получил `volumeMount` (subPath, readOnly) в `/app/app-config.yaml`. - [x] `manifests/tier-1/backstage/example.yaml` пере-рендерён. - [x] Sub-phase 1.6 status: ✅ shipped. **Acceptance:** `https://backstage.` открывается, виден catalog (пустой), `auth.providers.guest` пускает без логина (`dangerouslyAllowOutsideDevelopment: true`). **Зависит от:** 1.5 **Размер:** M (разбит на 3 цикла: k8s-манифесты `v0.1.18` ✅, app-скаффолд `v0.1.19` ✅, app-config + OAuth `v0.1.20` ✅) --- ### 1.7 Application CRD v1alpha1 ✅ > v0.1.25 — schema refactor: `base` + `environments` moved under `spec` for k8s-convention alignment before phase 1.8. **Цель:** зарегистрировать CRD `Application` в кластере, схема валидируется через CUE. **Поставка:** - [x] OpenAPI v3 схема CRD: hand-rolled YAML мирорит CUE `#ApplicationSpec` (v0.1.22 — `cli-providers::k8s::application_crd` + apply через cluster-bootstrap; `cue cmd export-crd` автогенерация откладывается до v0.2.x). - [x] Поля v1alpha1: `image`, `expose`, `replicas`, `env` (только литералы), `environments` map (v0.1.21 — schema + Rust parser). - [x] Admission webhook (Rust, axum + rustls) в отдельном pod с auto-rotated cert (cert-manager Certificate в `apprafter-system`, `cert-manager.io/inject-ca-from` синхронит `caBundle` на ValidatingWebhookConfiguration; v0.1.23 — webhook crate + Dockerfile, v0.1.24 — k8s-манифесты + cluster-bootstrap wiring). - [x] Невалидный manifest реджектится с понятной ошибкой (v0.1.24 — webhook возвращает `Application is invalid: : ` через AdmissionReview, kube-apiserver включает это сообщение в ответ `kubectl apply`). **Acceptance:** `kubectl apply` валидного Application проходит; невалидного — с сообщением, указывающим поле и причину. **Зависит от:** 0.4, 1.5 **Размер:** M --- ### 1.8 Application operator — каркас на kube-rs ✅ > v0.1.26 — sub-phase 1.8a shipped: 3 library crates (`operator-core` + `operator-rendering` + `operator-controllers/application`). > v0.1.27 — sub-phase 1.8b shipped: `apprafter-operator` binary + 3 Prometheus signals + axum `/healthz` / `/readyz` / `/metrics`. > v0.1.28 — sub-phase 1.8c shipped: Lease-based leader election (`operator-core::leader`). > v0.1.29 — sub-phase 1.8d shipped: Helm chart at `operator/charts/apprafter-operator/`. Phase 1.8 ✅. **Цель:** Rust-операторный pod с reconcile-loop по `Application`. **Поставка:** - [x] `operator/` — workspace с подпакетами `operator-core`, `operator-controllers/application`, `operator-rendering` (v0.1.26). - [x] Контроллер на `kube-rs`, leader election через Lease (v0.1.27 + v0.1.28). - [x] Метрики Prometheus: `reconcile_total`, `reconcile_duration`, `reconcile_errors` (v0.1.27). - [x] Структурированный лог (tracing) (v0.1.27 — `tracing-subscriber::EnvFilter` в `apprafter-operator/main.rs`). - [x] Health/readiness endpoints (v0.1.27 — `/healthz` + `/readyz` axum routes). - [x] Helm chart для деплоя оператора (v0.1.29 — `operator/charts/apprafter-operator/`). **Acceptance:** оператор запускается, видит Application-объекты, пишет «reconciled» в лог; metrics endpoint отвечает. **Зависит от:** 1.7 **Размер:** M --- ### 1.9 Application reconcile: image + expose + replicas ✅ > v0.1.30 — sub-phase 1.9a shipped: pure `render_application` for Deployment + Service. > v0.1.31 — sub-phase 1.9b shipped: reconcile applies children via SSA + updates `status`. > v0.1.32 — sub-phase 1.9c shipped: per-environment expansion (`APPRAFTER_ENV` selects override). Phase 1.9 ✅. HTTPRoute deferred to a later phase that owns Gateway domain config end-to-end. **Цель:** Application → Deployment + Service + HTTPRoute. **Поставка:** - [x] Renderer (pure-функция) `Application → [k8s Resource]` (v0.1.30 — Deployment + Service; HTTPRoute deferred to a phase that owns Gateway domain config end-to-end). - [x] Per-environment expansion (v0.1.32 — pure-Rust merge; functionally equivalent to CUE unification for our v1alpha1 schema, switchable to CUE FFI when CUE-only constructs are added). - [x] Apply-семантика: server-side apply с field manager `apprafter-operator` (v0.1.31). - [x] Status subresource: `phase`, `observedGeneration`, `conditions`, `endpointURL` (v0.1.31). - [x] Удаление Application удаляет дочерние ресурсы (ownerReferences) (v0.1.30). **Acceptance:** манифест Application с image+expose даёт работающий HTTP endpoint, доступный изнутри кластера; `curl` на endpoint отвечает. **Зависит от:** 1.8 **Размер:** M --- ### 1.10 Backstage Application plugin (status view) ✅ > v0.1.33 — sub-phase 1.10a shipped: TypeScript scaffold + types + pure handler stubs. > v0.1.34 — sub-phase 1.10b shipped: `KubeApplicationStore` proxies kube apiserver via in-cluster SA token. > v0.1.35 — sub-phase 1.10c shipped: applications-frontend scaffold + `ApplicationsApi` interface + pure `applicationsToRows` transform. > v0.1.36 — sub-phase 1.10d shipped: `ApplicationsTable` + `ApplicationDetail` + `EnvironmentTabs` React components + per-env helpers. Backstage `createApiRef` + `createPlugin` wiring documented as a consumer-side snippet (keeps the package's dep tree light enough to publish independently). Phase 1.10 ✅. **Цель:** в Backstage — список Application, статус, ссылка на endpoint, последние события. **Поставка:** - [x] Backstage backend plugin читает k8s API напрямую (через kubeconfig service account) (v0.1.33 + v0.1.34 — `@apprafter/applications-backend` с `KubeApplicationStore` через in-cluster service-account token). - [x] Frontend plugin: таблица + drilldown (v0.1.36 — `ApplicationsTable` + `ApplicationDetail` React components). - [x] События: replicas / status / последние deploys (v0.1.36 — `ApplicationDetail` рендерит `status.phase` + `status.observedGeneration` + полный список `conditions` с `lastTransitionTime`). - [x] Per-environment вкладки (dev/staging/prod) (v0.1.36 — `EnvironmentTabs` controlled component + `applicationsForEnvironment` filter helper). **Acceptance:** в Backstage виден задеплоенный hello-world, статус Ready, ссылка работает. **Зависит от:** 1.6, 1.9 **Размер:** M --- ### 1.11 Golden-path template: Bun HTTP service ✅ > v0.1.37 — sub-phase 1.11a shipped: `examples/templates/bun-http/` starter (OneBun + multi-stage Dockerfile + v1alpha1 Application.cue). > v0.1.38 — sub-phase 1.11b shipped: Backstage Software Template (`template.yaml` + `skeleton/`) + operator quickstart at `docs/dev-guide/quickstart.md`. Phase 1.11 ✅. **Цель:** Backstage Software Template, генерирующий стартер на OneBun. **Поставка:** - [x] Template в `examples/templates/bun-http/`: `package.json`, `Dockerfile` (multi-stage, distroless), `src/index.ts` + `app.module.ts` + `health.controller.ts` + `config.ts` (OneBun controllers + envSchema), `apprafter/Application.cue` (v0.1.37). - [x] Backstage software template manifest с параметрами (имя, репо, домен) (v0.1.38 — `template.yaml` + `skeleton/` subdir со scaffolder Nunjucks templating). - [x] Документация в `docs/dev-guide/quickstart.md` (v0.1.38). **Acceptance:** через UI Backstage за 3 клика создаётся репо с готовым стартером; коммит → Argo CD → задеплоилось. **Зависит от:** 1.10 **Размер:** S --- ### 1.12 End-to-end MVP smoke-тест ✅ > v0.1.39 — sub-phase 1.12a shipped: `e2e/mvp.sh` orchestration script + operator-guide quickstart. > v0.1.40 — sub-phase 1.12b shipped: `.github/workflows/nightly.yml` (cron 04:00 UTC + workflow_dispatch). Phase 1.12 ✅ pending operator's "5 greens in a row" judgment call — the automation lands here, the verdict lands when the streak holds. **Цель:** воспроизводимый E2E-тест полного пути: чистый Hetzner-аккаунт → задеплоенный hello-world. **Поставка:** - [x] Скрипт `e2e/mvp.sh`: `platform-cli init` → ждёт готовности → деплоит hello-world → проверяет HTTP-endpoint (v0.1.39 — Application-via-template путь живёт в operator-guide quickstart до публикации образа оператора). - [x] CI nightly job (с реальным Hetzner project под отдельный billing-tag) (v0.1.40 — `.github/workflows/nightly.yml`, cron 04:00 UTC + workflow_dispatch; billing-tag через `apprafter=true` label, выделенный CI tag отложен до propagation labels через provider). - [x] Таймер: фиксируем «time-to-first-application», цель < 30 минут (v0.1.39 — `START_NS` + `elapsed` в mvp.sh; observed 6-9 min, well under 30-min budget). - [x] `docs/operator-guide/quickstart.md` — те же шаги вручную (v0.1.39). **Acceptance:** nightly зелёный 5 раз подряд; ручной прогон по docs работает у нового человека. **Зависит от:** 1.11 **Размер:** M --- ### 1.13 Закрытие чек-листа M1 spec ✅ **Поставка:** - [x] Обновить `spec.md` §6 M1 — все пункты `[x]` (v0.1.41). - [x] Tag `v0.1.0-mvp` (v0.1.41 — the v0.1.41 commit also carries an annotated `v0.1.0-mvp` tag pointing at the same SHA). - [x] Release notes (v0.1.41 — `docs/changelog/history.md` graduates the Phase 1 section into a `v0.1.0-mvp` release block). **Размер:** XS --- ### 1.14 Level B integration cycle (default-on operator + webhook) ✅ > v0.1.64 — sub-phase 1.14 shipped: `cluster-bootstrap` installs the AppRafter operator + admission-webhook by default from ghcr.io images published by `release-operator.yml`. Default-on semantics with opt-out via `spec.{operator,admissionWebhook}.enabled: false`. Fork builds override via `image` + `tag` fields; variant-C resolution semantics (full-ref ignores `tag`). **Поставка (v0.1.64):** - [x] `cli-providers::k8s::image_ref` — `RELEASED_OPERATOR_VERSION` const + `resolve_image_ref` variant-C helper (6 unit tests). - [x] `cli-providers::k8s::operator_values` — pure values-YAML builder (3 unit tests). - [x] `cli-providers::k8s::operator_chart` — `include_dir!`-embedded helm chart + runtime extractor (2 tests). - [x] `cli-core::manifest` — `OperatorBlock` + extended `AdmissionWebhookBlock` (4 schema tests). - [x] `HelmUpgradeArgs.version` → `Option` (allows local-path chart installs). - [x] `perform_bootstrap` gains operator + webhook orchestration steps in `apprafter-system` (step 8 + step 9 per spec). - [x] 5 new orchestration tests (default install order; operator opt-out; webhook opt-out; full-ref override; tag-only override). - [x] CUE schema extension: `spec.operator?:` block + extended `spec.admissionWebhook?:`. - [x] `e2e/mvp.sh` Phase 6.5 — apply Application CR + poll status `Ready` + assert child Deployment `Available` (60s deadline). - [x] `docs/operator-guide/quickstart.md` §5 rewrite (operator pod required → operator pod installed by default). **Acceptance:** против чистого Hetzner кластера новый оператор проходит manual walk из spec §1.14 (init → apply → kubeconfig → cluster-bootstrap → kubectl apply Application → `.status.phase == Ready` за 60с → child Deployment живой) без `helm install` руками и без «build your own image». **Зависит от:** 1.13 **Размер:** S (один цикл, ~3 рабочих дня) --- ### 1.15 Level C GitOps cycle (env-driven Argo CD repo credentials) ✅ > v0.1.65 — sub-phase 1.15 shipped: `cluster-bootstrap` provisions the `apprafter-bootstrap-repo-creds` Argo CD Secret automatically when `APPRAFTER_ARGOCD_REPO_TOKEN` is set, enabling private GitHub/GitLab `spec.argocd.bootstrapRepo` without `kubectl apply` of a Secret by hand. Public-repo path unchanged. 4-quadrant manual walk documented in `docs/operator-guide/gitops-walk.md`. **Поставка (v0.1.65):** - [x] `cli-providers::k8s::argocd_repo_secret` — pure builder + 2 constants (4 unit tests). - [x] `cluster_bootstrap::read_argocd_repo_creds` testable helper over injected env-lookup closure (4 unit tests). - [x] `ClusterSettings` gains `argocd_repo_creds: Option<(String, String)>`; `default_cluster_settings` + `read_cluster_settings_from_manifest` populate it from env. - [x] `perform_bootstrap` gains `argocd_repo_secret_path: Option<&Path>` parameter at bootstrap step 9.5 (between webhook and Argo CD HTTPRoute). - [x] `run()` builds the tempfile when both creds + `bootstrap_repo` are `Some`, wires path through. - [x] Success-message suffix mentions repo-creds Secret when applied. - [x] 3 new orchestration tests (token+repo creates Secret before bootstrap App; token absent skips Secret but keeps App; no bootstrap repo skips both). - [x] 8 pre-existing orchestration tests updated for the new arg position. - [x] `docs/operator-guide/gitops-walk.md` — 4-quadrant runbook (GitHub × GitLab × public × private) with prereqs, DoD checklists, troubleshooting matrices, token-rotation + revoke sections. - [x] `docs/operator-guide/quickstart.md` §3 gains the env-var opt-in bullet. **Acceptance:** против чистого Hetzner кластера новый оператор проходит manual walk из spec §1.15 (все 4 квадранта end-to-end), каждый walk заканчивается зелёным DoD checklist (Argo CD UI: bootstrap = Synced + Healthy; child Application reconcilen оператором; для private — Secret присутствует в argocd ns). **Зависит от:** 1.14 **Размер:** S (один цикл, ~2 рабочих дня кода + manual walk) --- ## Фаза 1.5 — Self-managing platform rethink (M1.5) ⚡ > 🏁 SR: A · order 1 — M1.5 Track B subset (ADR 0025/0028/0029: GitOps + platform-stack distribution + CUE CMP); managed substrate, largely closed **Цель фазы:** архитектурный rethink из ADR 0025–0029. Переход от imperative `cluster-bootstrap` к Argo CD-managed platform stack из versioned OCI chart, declarative version control через PlatformStack CRD, unified MigrationPlan для application + platform scopes, CUE compilation для user app repos через CMP sidecar. **Spec:** §3.10, §3.11 (PlatformStack), §3.8 (MigrationPlan unified). ADRs 0025–0029. **Almighty target:** «happy path» first user experience сжимается до ~30 минут end-to-end (install binary → `apprafter init` → `apprafter bootstrap-all` → `apprafter open argocd` → add app repo via UI → app deployed). Каждая подфаза landing'ится как `v0.1.66`–`v0.1.83` patch release (loose recommendation, точное mapping commit-driven). После закрытия M1.5 — tag `v0.2.0-self-managing`, после чего Phase 2 (M2) стартует с `v0.2.0-services` уже на правильном фундаменте. **Numbering:** под-фазы M1.5 используют 1.66–1.83 как continuation Phase 1 namespace, поскольку landing'ятся последовательными `v0.1.66`–`v0.1.83` releases перед `v0.2.0-services`. Major version stays `0`; minor reflects phase number (`v0.2.x` для всего между M1.5 и M3 closure, `v0.3.x` для M3 series, etc.). **Blocks Phase 2** потому что Phase 2 (ServiceProviders, ResourceClaims, Tenant logic) builds on the GitOps-managed platform. Landing Phase 2 поверх split-brain дизайна приведёт к технического долгу — все ServiceProviders придётся reframing когда M1.5 doлжен будет landить позже. ### M1.5 Track positioning — CLI DX rework + Platform rethink + Dev-mode integration M1.5 содержит **три work tracks**, выполняемых **последовательно** в указанном порядке. Каждый track имеет свой authoritative spec: | Track | Order | Authoritative spec | Description | |---|---|---|---| | **A. CLI DX rework** | First | `cli-dx-task.md` §17 (12 items) | Target store, `apprafter target {add,list,use,...}`, `whoami`, `doctor`, `bootstrap-all` wrapper, miette errors, rename `platform-cli` → `apprafter`, aliases, color/NO_COLOR. **Prerequisite for Track B** потому что platform rethink relies on new CLI infrastructure (target resolution, bootstrap-all, manifest auto-discovery). | | **B. Platform rethink** | Second | this file, sub-phases **1.66 — 1.83** (18 items, numbered below) | Argo CD as control surface, PlatformStack CRD + Controller, MigrationPlan unification, CUE → OCI chart distribution, CMP for user app CUE. Lands after Track A is complete. | | **C. Dev-mode Phase 1B** | Third | `dev-mode-task.md` §20 Phase 1B | Minimum viable dev mode — `apprafter dev {cluster up, init, up, down, list, logs}` on local k3d. Lands after Track B closure (after `v0.2.0-self-managing` tag). | **Why this order**: - **Track A first**: bootstrap-all wrapper, target resolution, и miette errors are required for the minimal `cluster-bootstrap` rewrite (Track B 1.70) to provide acceptable UX. Без Track A, platform rethink landed бы на том же `cargo run --bin platform-cli` from-source workflow, который сейчас documented gap. Reverse order would create two sub-optimal user experiences during M1.5. - **Track B second**: platform rethink uses CLI infrastructure from Track A. Once `cluster-bootstrap` rewritten и PlatformStack CRD in place, M1.5 closes with `v0.2.0-self-managing` tag. - **Track C third**: dev-mode Phase 1B benefits from Track A CLI rework AND reuses Track B platform-stack chart's tier-1 overlay (with new `tiers/dev.cue` overlay). Lands after `v0.2.0-self-managing` as a follow-up patch series before M2 begins. **Dependencies between tracks (sequential)**: - Track A `target store` (`cli-dx-task.md` §5.1–5.6) → Track B `cluster-bootstrap` rewrite (1.70) requires target resolution instead of env vars. - Track A `bootstrap-all` orchestrator (`cli-dx-task.md` §5.11) → Track B 1.70 — these should be one cohesive piece of work, landed within Track A. - Track A `apprafter open` (referenced in Track B 1.79) → either land as part of Track A late items или Track B 1.79 — choose at implementation time when Track A is nearly done. - Track C dev-mode Phase 1B references Application CRD operator (already shipped в Phase 1 v0.1.7-v0.1.65), benefits from Track A CLI rework, и reuses Track B's `tiers/dev.cue` overlay. **M1.5 closure**: tag `v0.2.0-self-managing` after **both Track A and Track B** complete. Track C dev-mode Phase 1B lands as a follow-up patch series (e.g., `v0.2.1`, `v0.2.2` patch numbers depending on how it splits across commits). Phase 2B и Phase 3B из dev-mode-task.md лежат в later milestones (after M2 and M3 respectively). **Total M1.5 aggregate**: Track A (12 small-medium items per `cli-dx-task.md` §17) + Track B (18 items, 1 L + 7 M + 8 S + 2 XS) ≈ **L+ overall**, with Track C following as a separate ~M-aggregate series. The heavy work concentrates in Track B 1.73 (PlatformController — the only L item with distributed-systems penalty applied); most other items are S or M. --- ## Фаза 1.5 / Track A — CLI DX rework (`cli-dx-task.md` §17) > 12 sub-versions, one per `cli-dx-task.md` §17 row, landed as `v0.1.69`–`v0.1.80` patch releases. Each row owns a focused slice (feature + test + docs). Track B (sub-phases 1.66 onwards) **does not start** until Track A is closed — its `cluster-bootstrap` rewrite depends on the target store + `bootstrap-all` orchestrator + miette errors landed here. ### 1.66A.1 Rename `platform-cli` → `apprafter` + deprecation shim ✅ > v0.1.69 — sub-phase 1.66A.1 shipped: Cargo package + binary flipped to `apprafter`; legacy `platform-cli` survives as a deprecated shim that warns + forwards; user-facing docs swept. **Source:** `cli-dx-task.md` §12 + §17 row 1. **Цель:** перевести user-facing binary с легаси-имени `platform-cli` на каноничное `apprafter` без слома существующих скриптов. Foundation для всех остальных Track A под-фаз (target store, `bootstrap-all`, `doctor`, `whoami`), которые landятся последовательно в `v0.1.70`–`v0.1.80`. **Поставка:** - [x] `cli/platform-cli/Cargo.toml` — package переименован в `apprafter`; `[[bin]] name = "apprafter"` (path `src/main.rs`) — каноничная точка входа; второй `[[bin]] name = "platform-cli"` (path `src/bin/platform-cli.rs`) — shim, помеченный к удалению в `v0.2.0`. - [x] `cli/platform-cli/src/bin/platform-cli.rs` — shim: печатает 3-строчный deprecation warning на stderr, потом `Command::new(apprafter)` + `.args(skip(1))` + forward exit code; cross-platform (`.exe` suffix на Windows). - [x] `cli/platform-cli/src/cli.rs` — `#[command(name = "apprafter", ...)]` для clap-help; about-line обновлён. - [x] `cli-core::logging::init` — дефолтный `EnvFilter` теперь `warn,apprafter=info,cli_core=info,cli_state=info,cli_providers=info`; без этого фикса INFO-логи фильтровались после переименования крейта (regression поймана `cli_smoke::tracing_logs_go_to_stderr_not_stdout`). - [x] User-facing error hints (`run \`platform-cli init …\` first` и т.п.) в `commands/apply.rs`, `commands/argocd_password.rs`, `commands/cluster_bootstrap.rs`, `commands/import.rs`, `commands/kubeconfig.rs` теперь ссылаются на `apprafter`. - [x] Internal docstrings + Cargo descriptions в `cli/cli-core`, `cli/cli-providers`, `cli/cli-state` обновлены — grep-discoverability осталась консистентной. - [x] Все 4 integration-теста (`cli_smoke`, `argocd_password_test`, `import_test`, `kubeconfig_test`, `cluster_smoke_test`) переключены на `Command::cargo_bin("apprafter")`. - [x] Новый regression-guard `cli_smoke::platform_cli_shim_warns_and_forwards` — пинит обе половины контракта shim'а (deprecation banner на stderr + forwarded `plan` output untouched на stdout + exit code). - [x] User-visible docs sweep: `README.md`, `cli/README.md`, `e2e/{README.md,mvp.sh}`, `operator/{README.md,charts/apprafter-operator/README.md}`, `backstage-plugins/host/{README.md,scripts/scaffold.sh}`, `manifests/**/README.md`, `examples/templates/bun-http/**`, `schemas/v1alpha1/{infrastructure,infrastructureproviderplugin}.cue`, `docs/{architecture,dev-guide,operator-guide,reference}/**/*.md`, `.github/ISSUE_TEMPLATE/bug.yml`, `SECURITY.md`, `.gitignore` — все ссылаются на `apprafter`. - [x] `spec.md` swept (kept `cli/platform-cli/` dir name in Appendix A repository tree with explicit comment that dir is renamed in `v0.2.x`). - [x] `docs/changelog/history.md` — new `v0.1.69` block с Changed/Added/Docs/Backwards-compatibility секциями; historic v0.1.x entries сохранены as-is (no rewriting of past system state). **Acceptance:** - ✅ `cargo build --workspace` зелёный — обе bin entry (`apprafter` + `platform-cli`) компилируются. - ✅ `cargo test --workspace` зелёный (61+ unit + 16 integration, включая новый shim-test). - ✅ `cargo clippy --workspace --all-targets -- -D warnings` зелёный. - ✅ `cargo fmt --all -- --check` зелёный. - ✅ Manual walk (см. ниже) — `apprafter --help` работает, shim прячет deprecation warning на stderr + forwards exit code, существующие env-var-based workflows (`HCLOUD_TOKEN=… apprafter init …`) функционируют без изменений. **Out-of-scope (отложено в следующие Track A слоты):** - Persistent target store (`apprafter target {add,list,use,show,rename,remove}`) — Track A.2/A.3. - Interactive wizard через `inquire` + miette-diagnostic errors — Track A.4. - `apprafter doctor`, `apprafter whoami` — Track A.6/A.7. - `apprafter bootstrap-all` orchestrator — Track A.9. - `apprafter auth` stubs — Track A.6. - Aliases (`apprafter t`), `--color` flag, `NO_COLOR` support — Track A.11. - ADR `docs/adr/0014-cli-command-structure.md` — Track A.12 (после всей Track A landed). - Переименование dir `cli/platform-cli/` → `cli/apprafter/` — отложено до `v0.2.0-self-managing` (M1.5 closure) одной cleanup-коммитой. **Зависит от:** 1.65 (последняя закрытая Track-B-prerequisite). **Размер:** S (один цикл, ~0.5 рабочего дня механического свипа + один новый файл shim + один regression-guard тест). --- ### 1.66A.2 Target file structure + IO module ✅ > v0.1.72 — sub-phase 1.66A.2 shipped: `cli-core::target` module — types (`GlobalConfig`, `TargetConfig`, `TargetCredentials`, `Target`, `TargetStorePaths`) + atomic load/save IO + 0600 enforcement on credentials. No CLI commands wired yet — pure foundation for A.3+ wizards. **Source:** `cli-dx-task.md` §4 (file layout), §8 (deps), §17 row 2. **Поставка:** - [x] Новые workspace deps: `serde_yaml = "0.9"` + `dirs = "5"`; `tempfile` промотан из dev-dependencies cli-core в обычные (atomic-write использует его в prod-коде). - [x] `cli-core/src/target.rs` (~570 строк): - `default_config_root()` → `dirs::config_dir().join("apprafter")` — cross-platform XDG. - `TargetStorePaths { root }` testable locator с методами `global_config_file`/`targets_dir`/`target_dir`/`target_config_file`/`target_credentials_file`/`auth_dir`/`auth_keep_file`/`state_dir` — миррорит spec §4 на тип-уровне. - `GlobalConfig { active_target, version }` с `TARGET_STORE_VERSION = 1` форвард-compat кодом. - `TargetConfig { provider, region, default_tier, cluster_name, ssh_key_path }` (`#[serde(default)]`). - `TargetCredentials { hetzner_token: Option }` — **manual `Debug` impl** с `` маркером (никогда не derive — лекит токен в любом `println!("{:?}", ...)`). - `load_global_config`/`save_global_config`/`load_target`/`save_target`/`list_target_names`/`remove_target`. - `atomic_write(path, bytes, secret)` — tempfile-in-same-dir + fsync + chmod (0600 для secret, 0644 для public) + `persist()` rename (POSIX-atomic, ReplaceFileW на Windows). - `ensure_auth_placeholder()` — создаёт `auth/.keep` на любом первом write, чтобы reserved namespace existed для будущего Managed. - [x] `cli-core/src/error.rs`: новые варианты `InvalidTargetConfig { path, message }`, `TargetNotFound { name, available }`, `Yaml(serde_yaml::Error)` через `#[from]`. - [x] `cli-core/src/lib.rs`: pub use re-export всех target-типов и функций; модуль `target` зарегистрирован. - [x] 16 regression-guard unit-tests (inline в `target.rs`): - `default_config_root_points_at_user_config_dir_under_apprafter` — leaf path sanity guard. - `paths_compose_per_spec_directory_layout` — пин on-disk shape против spec §4 (если кто-то ренеймит `TARGETS_DIR` константу, тест отлетит). - `load_global_config_returns_none_on_fresh_store` — first-run case ОК. - `save_then_load_global_round_trips_active_target` — round-trip global. - `save_global_creates_auth_placeholder_directory` — auth/.keep всегда создаётся. - `load_global_config_returns_invalid_target_config_on_corrupt_yaml` — corrupt YAML → typed error. - `save_then_load_target_round_trips_both_halves` — round-trip per-target config + creds. - `load_target_returns_target_not_found_with_available_list` — error message включает comma-separated список существующих имён. - `load_target_tolerates_missing_credentials_file` — dotfiles-only сценарий (config есть, credentials нет — возвращает empty creds, не ошибку). - `credentials_file_lands_at_mode_0600` (Unix-only `#[cfg(unix)]`) — пин разрешений: credentials.yaml = 0600, config.yaml = 0644. - `list_target_names_returns_empty_on_fresh_store` + `list_target_names_returns_sorted_names_skipping_dot_dirs` — список target'ов сортирован, hidden dirs (`.scratch` от atomic-write tempfiles) скрыты. - `remove_target_deletes_both_files_and_state_dir` — удаление каскадно сносит `state//`. - `remove_target_returns_target_not_found_when_missing` — idempotency помощник. - `credentials_debug_redacts_token` — пинит `` маркер в Debug формате (защита от случайного println). - `atomic_write_leaves_no_temp_files_on_success` — после успешного save в корне нет `.apprafter-tgt-*.tmp` файлов. **Acceptance:** - ✅ `cargo build --workspace` зелёный (новые `serde_yaml`, `dirs`, `tempfile` deps скомпилированы). - ✅ `cargo test --workspace`: 26 cli-core тестов (16 новых target + 10 pre-existing), 0 failures across весь workspace. - ✅ `cargo clippy --workspace --all-targets -- -D warnings` — clean. - ✅ `cargo fmt --all -- --check` — clean. - ✅ `scripts/check-spdx-headers.sh` (150 файлов) + `scripts/lint-cue.sh` — green. - ✅ Module re-exports проверены: `use cli_core::{TargetStorePaths, GlobalConfig, Target, TargetConfig, TargetCredentials, save_target, load_target, list_target_names, remove_target}` компилируется в чистом downstream crate'е (semantics test through smoke tests in Track A.3). **Out-of-scope (отложено в зависимые слоты):** - Никаких CLI команд — `apprafter target add/list/use/...` приходят в Track A.3 (non-interactive) → A.4 (interactive wizard) → A.5 (CRUD-набор). - Provider validator framework (`token format regex`, API ping) — Track A.4. - Resolution chain plumbed в existing commands (`init/apply/cluster-bootstrap` берут токен из active target) — Track A.8. - Migration существующего `/.apprafter/state.json` в per-target `/state//` — Track A.8. - `secrecy::Secret` wrapper для in-memory защиты — добавится в Track A.3/A.4 когда credentials handling попадает в hot path. Сейчас защита через manual Debug-redact + mode 0600 на файле. **Зависит от:** 1.66A.1 ✅ **Размер:** S (один цикл, ~1 рабочий день кода + тестов). --- ### 1.66A.3 `apprafter target add` non-interactive ✅ > v0.1.73 — sub-phase 1.66A.3 shipped: clap subcommand `apprafter target add ` (+ alias `apprafter t add`) с pure flag-driven flow, валидация (name shape, provider whitelist, token format, ssh-key readable), `--force` / `--renew` семантика, первый target auto-promotes в active. Interactive wizard via `inquire` — отложен в A.4. **Source:** `cli-dx-task.md` §5.1 (non-interactive flow), §10 (error patterns), §11 (validation rules), §17 row 3. **Поставка:** - [x] `cli/platform-cli/src/cli.rs` — новый `Commands::Target { action }` варинт + `TargetCommand::Add { name, provider, token, ssh_key, region, tier, cluster_name, force, renew, no_interactive }` enum-вариант. `#[command(alias = "t")]` на `Target` группе → `apprafter t add …` работает идентично `apprafter target add …`. `--token` читает `HCLOUD_TOKEN` env var через `#[arg(env)]`, `--ssh-key` — `APPRAFTER_SSH_PUBLIC_KEY_PATH`. `--force` и `--renew` помечены `conflicts_with` для clap-уровневого reject комбинации. - [x] `cli/platform-cli/src/commands/target.rs` — handler-модуль: - `run(action) → match → run_add(args)`. - `validate_target_name(name)` — non-empty, ≤64 chars, `[A-Za-z0-9-]+`, без leading/trailing `-`. - `require_known_provider(opt)` — non-None + whitelist (`["hetzner-cloud"]` пока). - `require_token(provider, opt)` — non-None + per-provider format check. Для hetzner-cloud зовёт `cli_core::target::validate_hetzner_token_format`. - `verify_ssh_key_readable(path)` — `path.exists()` + `read_to_string` success. - `run_renew(paths, args)` — error если target отсутствует; refuses конфиг-флаги (только credentials); зовёт `save_target(existing.with(new creds))`. - `ensure_active_target(paths, name)` — `save_global_config({active_target: name})` только если `load_global_config` возвращает None (first-run case); subsequent saves не трогают active pointer. - [x] `cli/platform-cli/src/main.rs` — `match args.command` (move) + dispatch `Commands::Target { action } => commands::target::run(action)?`. Существующие command::run сигнатуры (`&str` / `bool`) совместимы через `&owned` / Copy. - [x] `cli-core::target` extensions: - `CONFIG_DIR_ENV = "APPRAFTER_CONFIG_DIR"` — env override для `default_config_root()` (testing ergonomics + power-user redirect). Используется verbatim, без `.join("apprafter")` суффикса. - `validate_hetzner_token_format(token)` — `^hcloud_[a-zA-Z0-9]{60,}$` без `regex` crate (string-методы достаточно для такой простой проверки). Возвращает `Result<(), String>` — caller-level wrap в `CliError::Other` с context'ом. - [x] 17 integration-тестов (`tests/target_test.rs`): - happy path (`writes_config_and_credentials_and_promotes_first_target_to_active`) — пинит на-disk layout, active pointer, token landed. - mode 0600 (`credentials_file_is_mode_0600`, Unix-only). - env-var fallback (`uses_hcloud_token_env_var_as_fallback`) — clap `#[arg(env)]` works. - missing token (`errors_when_token_missing_entirely`). - unknown provider (`errors_on_unknown_provider`). - malformed token (`errors_on_malformed_hetzner_token`). - invalid name (`errors_on_invalid_target_name`). - no-force overwrite reject (`refuses_to_overwrite_existing_target_without_force`) — error message включает оба `--force` и `--renew` hint'а. - force overwrite (`force_overwrites_existing_target_and_keeps_active_pointer`) — active pointer **не** меняется на second-save. - renew rotate (`renew_rotates_credentials_without_touching_config`). - renew on missing (`renew_on_missing_target_errors_with_hint`). - renew refuses config flags (`renew_rejects_config_flags`). - clap-level conflict (`force_and_renew_are_mutually_exclusive`). - ssh-key path verified (`with_ssh_key_path_verifies_file_exists`). - ssh-key missing (`errors_when_ssh_key_path_missing`). - second save preserves active (`second_target_save_keeps_first_as_active_and_reports_so`). - alias `t` works (`target_alias_t_subcommand_resolves_to_target`). - [x] 10 unit-тестов inline в `commands/target.rs` (pure validators): `validate_target_name` × 5 cases, `require_known_provider` × 2, `require_token` happy + bad, `verify_ssh_key_readable` × 2. - [x] 6 unit-тестов в `cli_core::target` (валидатор + env override): `default_config_root_honours_apprafter_config_dir_env_override`, `_ignores_empty_env_override`, `validate_hetzner_token_format` × 4. **Acceptance:** - ✅ `apprafter target add default --provider hetzner-cloud --token hcloud_…` создаёт целый target + ставит active. - ✅ Subsequent `target add another --provider … --token …` создаёт, **не** трогает active. - ✅ `target add existing` без `--force` → error с hint'ом на оба `--force` и `--renew`. - ✅ `target add existing --force` → overwrites; `target add existing --renew --token …` → только credentials, config preserved. - ✅ `apprafter t add … ` — alias работает. - ✅ `--force --renew` одновременно — clap reject (`conflicts_with`). - ✅ Malformed token / unknown provider / invalid name — typed error на stderr. - ✅ `cargo test --workspace`: 0 failures (17 new integration + 10 unit + 6 cli-core validator = +33 vs v0.1.72). fmt + clippy + SPDX (151 files) — clean. **Out-of-scope (отложено):** - Interactive wizard через `inquire` — Track A.4 (v0.1.74). - `apprafter target list / use / show / rename / remove` — Track A.5. - `apprafter target add` real API ping (`GET /v1/locations` against Hetzner) — Track A.4 validator framework. - `apprafter whoami` aggregator (показать active target + provider verified status) — Track A.6. - Resolution chain в существующие `init / apply / cluster-bootstrap` — Track A.8. **Зависит от:** 1.66A.2 ✅ (target store IO). **Размер:** S (один цикл, ~1 рабочий день). --- ### 1.66A.4 Provider validator framework + Hetzner API ping ✅ > v0.1.75 — sub-phase 1.66A.4 (split as A.4a) shipped: `cli-providers::validators` module с `ProviderValidator` trait + `HetznerCloudValidator` (`GET /v1/locations`); `target add` теперь делает real API ping по умолчанию + flag `--no-ping` / env `APPRAFTER_NO_PING` для CI/offline. Interactive wizard через `inquire` — отложен в **1.66A.4b** (v0.1.76). **Source:** `cli-dx-task.md` §5.1 (token verified step) + §11 (validation framework), §17 row 4. **Поставка:** - [x] `cli/cli-providers/src/hetzner_cloud/types.rs` — новые wire-types `Location { id, name, description, country, city, network_zone }` + `LocationListResponse`. Re-exported через crate root. - [x] `cli/cli-providers/src/hetzner_cloud/client.rs::list_locations()` — `GET /v1/locations` с тем же error-mapping шаблоном как остальные list_X методы (на 2xx parse, на 4xx/5xx → `CliError::Hetzner`, на transport-fail → `CliError::Other`). Reused validator-ом и (будущим в A.4b) wizard-region-picker'ом. - [x] `cli/cli-providers/src/validators.rs` — новый модуль: - `pub trait ProviderValidator { fn validate_credentials(&self) -> Result<()>; }` — минимальная поверхность (region/type lookups придут с wizard'ом в A.4b). - `pub struct HetznerCloudValidator { client: HetznerCloudClient }` + `new(base_url, token)` + impl `ProviderValidator` через `self.client.list_locations().map(|_| ())`. - 3 unit-теста с mockito: 200 OK (валид) / 401 → typed `CliError::Hetzner` / closed-port → `CliError::Other` transport. - [x] `cli-providers::lib.rs` — re-export `HetznerCloudValidator` + `ProviderValidator`. - [x] `cli/platform-cli/src/cli.rs::TargetCommand::Add` — новый флаг `--no-ping` с env-binding `APPRAFTER_NO_PING` через `BoolishValueParser` (принимает `1/0/yes/no/true/false/on/off` — не только canonical `true`/`false`, чтобы shell-сcripty `APPRAFTER_NO_PING=1 apprafter target add ...` работал). - [x] `cli/platform-cli/src/commands/target.rs::ping_provider(provider, token)` — orchestrator: знает per-provider маршруты, для hetzner-cloud зовёт `HetznerCloudValidator::new(hcloud_base_url(), token).validate_credentials()`. Error-mapping расширен с human-readable hint'ами: - 401 → "Hetzner Cloud rejected the token (HTTP 401)..." - non-401 HTTP error → "Hetzner Cloud API ping failed (HTTP {status})..." + reassurance что target NOT saved. - transport-fail → "could not reach Hetzner Cloud at {base}..." + `--no-ping` hint. - [x] `run_add` теперь делает ping после format/ssh-key checks но ДО save (так, чтобы failed ping не оставлял half-state на диске). `run_renew` — analogous, ping после `require_token`. - [x] Success-message в `target add` теперь упоминает статус: `... (token verified against Hetzner Cloud)` или `... (token NOT verified — \`--no-ping\` was passed)`. Closes the cli-dx-task.md §5.1 "✓ Token verified" UX promise on the non-interactive flow (interactive wizard will reuse the same string in A.4b). - [x] 5 новых integration-тестов (`tests/target_test.rs`): - `target_add_pings_provider_by_default_and_announces_verified_status` — mockito 200, target saved + UI says "verified". `mockito::Mock::expect(1)` пинит, что ping реально был сделан. - `target_add_surfaces_typed_error_on_hetzner_401` — mockito 401 + assertion что target dir на диске **не** создан (нет half-state). - `target_add_surfaces_helpful_error_when_api_is_unreachable` — closed port 1 → error message содержит либо "could not reach" либо "API ping failed" (зависит от платформы) + `--no-ping` hint. - `target_add_no_ping_flag_skips_validator_and_announces_unverified` — `--no-ping` короткозамыкает на нereachable base URL. - `target_add_no_ping_env_var_also_skips_validator` — `APPRAFTER_NO_PING=1` равноценен флагу. - [x] 17 prior integration-тестов получили `APPRAFTER_NO_PING=1` через sed-инжект (focus тех тестов — file IO / clap parsing, не API; новые ping-тесты эту поверхность покрывают отдельно). **Acceptance:** - ✅ `apprafter target add --token ` ходит в Hetzner, success-message содержит "(token verified against Hetzner Cloud)". - ✅ С невалидным токеном — typed error "(HTTP 401)" + target **не** сохранён. - ✅ С недоступным API — typed error + `--no-ping` hint. - ✅ `--no-ping` / `APPRAFTER_NO_PING=1` — short-circuit, success-message "(token NOT verified)". - ✅ Existing env-var-based `apply` / `cluster-bootstrap` flow не затронут — ping живёт только в `target add`. - ✅ `cargo test --workspace`: 22 target_test (17 prior +5 new) + 118 cli-providers (115 +3 validator) — 0 failures. fmt + clippy + SPDX (153 files) — clean. **Out-of-scope (отложено):** - Interactive wizard через `inquire` — Track 1.66A.4b (v0.1.76). - Region validator + region-picker (использует уже готовый `list_locations`) — пойдёт в A.4b вместе с wizard'ом. - `secrecy::Secret` обёртка для in-memory tokens — A.10/A.11 (miette + secret hardening pass). - Resolution chain в operational `init/apply/cluster-bootstrap` — A.8. **Зависит от:** 1.66A.3 ✅ (target add non-interactive). **Размер:** S (один цикл, ~1 рабочий день). --- ### 1.66A.4b Interactive wizard via `inquire` ✅ > v0.1.76 — sub-phase 1.66A.4b shipped: `commands::target_wizard` модуль с `inquire`-based prompts (Text/Password/Select), default-when-TTY поведение, inline validation внутри token prompt'а (format + API ping), region-picker через `list_regions()`. `--no-interactive` отключает wizard явно; CRUD команды (`list/use/show/...`) и `whoami`/`doctor`/`bootstrap-all` — следующие итерации. **Source:** `cli-dx-task.md` §5.1 (interactive flow) + §9 (TTY detection) + §17 row 4. **Поставка:** - [x] Workspace dep `inquire = "0.7"`; `dirs` поднят в platform-cli (использует для `~/.ssh/id_ed25519.pub` default'а и tilde expansion в SSH-key prompt). - [x] `cli-providers::validators::ProviderValidator` расширен `fn list_regions() -> Result>`. `RegionInfo { name, description }` с `Display` impl ` — ` для удобного scanning в `Select`. `HetznerCloudValidator::list_regions()` мапит `client.list_locations()` → отсортированный по `name` Vec. +2 mockito-тестa (sorted output + Display fallback на empty description). - [x] `cli/platform-cli/src/cli.rs`: positional `name` теперь `Option` — wizard может его спросить; ошибка surface'ится после wizard'а если name всё ещё None (non-TTY/`--no-interactive` сценарий). - [x] `commands/target.rs::check_target_name(&str) -> Result<(), String>` — pure helper экспортирован для wizard'а (validation сообщения консистентны между CLI surface и `inquire::Validation::Invalid`). `validate_target_name` остаётся как CliError-обёртка. - [x] `commands/target_wizard.rs` — новый модуль: - `should_use_wizard(no_interactive, stdin_tty, stdout_tty, all_required_present)` — pure decision, testable. Wizard fires только когда **обе** consoli TTYs И `--no-interactive` не передан И хотя бы один required input отсутствует. Если все флаги supplied — non-interactive path даже на TTY (respect explicit intent). - `run_add_wizard(initial: &AddArgs) -> Result` — последовательность из 6 prompts по spec §5.1: 1. **Target name** — `Text` с default `default`, валидатор вызывает `check_target_name`. 2. **Provider** — `Select` (сейчас одна опция `hetzner-cloud`, оставлен Select-shape для forward-compat). 3. **Provider token** — `Password` с `PasswordDisplayMode::Masked`. Inline-валидатор сначала проверяет формат, потом, если не `--no-ping`, делает API ping через `HetznerCloudValidator::validate_credentials()`. Failure → `Validation::Invalid("…")` → инквайр перепросит. Success → eprintln `✓ Token verified`. 4. **SSH public key path** — `Text` с default `/.ssh/id_ed25519.pub` (через `dirs::home_dir`); пустая строка = "skip". Tilde expansion `~/...` через `expand_tilde` helper. 5. **Default region** — `Select` из `validator.list_regions()` (когда token verified). При `--no-ping` fallback на `Text` с default `nbg1` (нет API → нет picker'а). 6. **Default tier** — `Select` по spec choices: `solo / team / prod / regulated` с `Display` impl ` — `. - `run_renew_wizard(provider, no_ping)` — упрощённый prompt только токена (config preserved). - Все prompts мапят `InquireError::OperationCanceled/Interrupted` → `CliError::Other("wizard aborted by user")` (Ctrl-C / Esc не дают backtrace). - [x] `commands/target.rs::run_add`: - Сначала evaluate `should_use_wizard(...)`, если true — `run_wizard_into_args(&mut args)` заполняет missing поля. - После wizard'а (или без него) `name` обязан быть `Some`; иначе typed error "target name required — pass it as a positional argument ... or run on a TTY". - `--renew` wizard ветвится отдельно: если name отсутствует — спрашиваем сначала name (Text + `check_target_name`), потом загружаем existing target для определения provider'а, потом `run_renew_wizard` для нового токена. - Save-time ping остаётся (re-verifies даже когда wizard уже ping'нул — cheap ~200ms, save-time check — authoritative). - [x] 5 unit-тестов inline в `target_wizard.rs` (pure helpers): `should_use_wizard` (4 ветки), `expand_tilde` (3 cases: `~/`, abs path, `~user/` not expanded), `inline_ping_error` (401 vs 5xx сообщения), `validate_for_provider` (good/bad/unknown), `TierChoice` Display. **Acceptance:** - ✅ `apprafter target add` на TTY → wizard просит все поля по порядку, inline-показывает `✓ Token verified` после успешного ping'а. - ✅ `apprafter target add work --provider hetzner-cloud --token ` на TTY → wizard НЕ fires (все required supplied), сразу не-interactive path. - ✅ `apprafter target add work --no-interactive` без token'а → typed error "is required" (TTY не помогает когда явно non-interactive). - ✅ Pipe / CI → no TTY → wizard skipped, как раньше. - ✅ Esc / Ctrl-C во время wizard'а → "wizard aborted by user" (без backtrace, exit-code 1). - ✅ `cargo test --workspace`: 22 target_test (без изменений) + 5 target_wizard unit + 64 cli-core + 120 cli-providers (+2 list_regions) — 0 failures. - ✅ fmt + clippy + SPDX (154 files) + CUE — clean. **Out-of-scope (явно отложено):** - E2E wizard testing с PTY-harness — overkill для текущего MVP; manual walks покрывают prompt UX. - "✓ Token verified (account: …, project: …)" detail per `cli-dx-task.md` §5.1 — Hetzner `/v1/locations` не возвращает account info, нужен `/v1/me`-style endpoint (Hetzner такого не имеет). Текущий "✓ Token verified" — достаточный signal. - `apprafter target list / use / show / rename / remove` — Track 1.66A.5. - `secrecy::Secret` обёртка для tokens in-memory — A.10/A.11 hardening pass. **Зависит от:** 1.66A.4a ✅ (validator framework + API ping). **Размер:** M (один цикл, ~1.5 рабочих дня). --- ### 1.66A.5 Target CRUD — `list / use / show / rename / remove` ✅ > v0.1.79 — sub-phase 1.66A.5 shipped: 5 новых subcommand'ов поверх target store (`tabled`-based table в `list`, kubectl-style `use/show`, `rename` с FS move + active-pointer обновлением, `remove` с `--yes` opt-in или interactive confirm). v0.1.77 + v0.1.78 wizard polish — затрагивает только `target add`; CRUD-набор полностью отдельный. **Source:** `cli-dx-task.md` §5.2–§5.6 + §6 (aliases) + §17 row 5. **Поставка:** - [x] Workspace dep `tabled = "0.15"` (для `target list` рендера); promoted в platform-cli как direct dep. - [x] `cli_core::target::rename_target(paths, from, to)` — атомарный `fs::rename` target-директории + best-effort move per-target state cache (`state//`). Refuses на missing-source (`CliError::TargetNotFound`) и existing-destination (`CliError::Other`). Re-exported через crate root. 4 unit-теста: happy path с state cache + missing source + dest collision + no-state-cache path. - [x] `cli/platform-cli/src/cli.rs`: новые `TargetCommand::{List, Use, Show, Rename, Remove}` варианты per spec §5.2–5.6. `Remove` имеет `--yes` flag. - [x] `cli/platform-cli/src/commands/target.rs`: - **`run_list`** — собирает rows через `list_target_names` + `load_target` per name (skip-with-tracing-warn на unreadable, не валит вся листинг). Tabled-derive struct `TargetListRow { active, name, provider, region, tier }` с `Style::sharp()` (чистая ASCII-таблица). Empty store → onboarding hint "apprafter target add". Trailing summary `N targets configured. Active: ''.`. - **`run_use(name)`** — validates target exists (через `load_target`), updates `GlobalConfig.active_target` через `save_global_config`. Polite no-op message если уже active. - **`run_show(name)`** — `name` Optional, default → active. Если no-active + no-name → typed error с hint'ом. Печатает Provider/Region/Default tier/Cluster name/SSH key/Hetzner token (через `token_summary(opt)` который выдаёт `"set (N chars; read credentials.yaml for the raw value)"` или `"not set"` — НЕ echo'ит токен). Trailing — на-диске пути config.yaml + credentials.yaml (mode 0600). - **`run_rename(from, to)`** — validates `to` через `check_target_name`, refuses identical from==to, вызывает `cli_core::target::rename_target`, потом если `active_target == from` — обновляет global config на `to`. - **`run_remove(name, yes)`** — `load_target` для existence + canonical TargetNotFound hint. Если `!yes`: на TTY показывает `inquire::Confirm` (default `false`), на non-TTY refuses ("non-interactive invocation: pass `--yes` to confirm ..."). После `remove_target`: если был active — pointer ре-assigned на alphabetically next remaining target; если targets закончились — `config.yaml` deleted (фреш-сторе-поведение возвращается). - [x] `token_summary` pure helper + unit-тест: НЕ leak'ит byte'ы токена даже частично. **Тесты (16 новых integration в `target_test.rs` + 4 cli-core unit + 1 platform-cli unit = 21 total):** - `target_list_on_empty_store_prints_onboarding_hint` - `target_list_renders_table_with_active_marker_and_columns` - `target_use_switches_active_pointer_and_reports_the_swap` - `target_use_on_already_active_is_a_polite_noop` - `target_use_on_missing_target_surfaces_available_hint` - `target_show_with_no_args_renders_active_target_with_masked_token` (пинит что token НЕ появляется в output) - `target_show_with_explicit_name_renders_named_target_without_active_marker` - `target_show_on_empty_store_errors_with_onboarding_hint` - `target_rename_moves_files_and_updates_active_pointer` - `target_rename_non_active_target_leaves_active_pointer_alone` - `target_rename_refuses_when_destination_exists` - `target_rename_rejects_invalid_destination_name` - `target_rename_refuses_identical_source_and_destination` - `target_remove_with_yes_flag_deletes_and_reassigns_active_alphabetically` - `target_remove_last_target_clears_active_pointer` - `target_remove_non_active_target_keeps_active_pointer_intact` - `target_remove_non_interactive_without_yes_refuses` - `target_remove_on_missing_target_surfaces_available_hint` - + `token_summary` unit - + 4 `rename_target` cli-core unit-тестов **Acceptance:** - ✅ `apprafter target list` рисует таблицу с `*` маркером на active, или onboarding hint на empty store. - ✅ `apprafter target use ` свитчит active; missing → friendly error c available-listом. - ✅ `apprafter target show [name]` показывает details; токен замаскирован как `set (N chars; ...)` без leak'а. - ✅ `apprafter target rename ` атомарен (либо обе директории на месте при collision, либо ровно одна после успеха), active-pointer follows automatically. - ✅ `apprafter target remove ` требует `--yes` на non-TTY, prompt'ит на TTY; удаление active → reassign alphabetically. - ✅ `apprafter t list/use/show/rename/remove` alias works (через существующий `#[command(alias = "t")]` на `Target`). - ✅ `cargo test --workspace`: 36 target_test (16 новых CRUD + 20 prior) + 74 cli-core (+4 rename) — 0 failures. fmt + clippy + SPDX (155 files) — clean. **Out-of-scope (отложено):** - "Last used" / "Account" / "Cluster status" колонки в `list` + `show` — нужна telemetry-wire-up через A.8 (operational commands записывают `last_used_at`) и/или Hetzner `/v1/account`-style endpoint которого у Hetzner нет публично. - ADR `docs/adr/0014-cli-command-structure.md` про resource-first grouping + auth namespace — Track A.12 (docs+ADR final pass). - `apprafter whoami` / `apprafter auth …` (stub) — Track A.6. **Зависит от:** 1.66A.4b ✅ (wizard) — используем тот же target store API. **Размер:** M (один цикл, ~1 рабочий день кода + tests). --- ### 1.66A.6 `apprafter whoami` + `auth` stubs ✅ > v0.1.80 — sub-phase 1.66A.6 shipped: top-level `apprafter whoami` (identity + active target + verified status) + hidden `apprafter auth login/logout/status` stubs (per spec §3.1 reserved namespace для Managed AppRafter Cloud). **Source:** `cli-dx-task.md` §5.7 (auth stubs) + §5.8 (whoami) + §3.1 (two-layer identity/target model) + §17 row 6. **Поставка:** - [x] `apprafter whoami` — новая top-level команда с одним флагом `--no-ping` (+ env `APPRAFTER_NO_PING` через `BoolishValueParser`). Рендер: - `Identity: anonymous (self-hosted mode)` — placeholder до Track A.10+ когда Managed Cloud auth wires in. - `Target: (active)` или onboarding hint на empty store. - `Provider: hetzner-cloud ()` — статус: `verified ✓` / `verified ✓` skipped (если `--no-ping`) / `verification failed ✗ — token rejected (HTTP 401). Run \`apprafter target add --renew\` ...` / `... HTTP from provider API` / `... provider unreachable (network?)`. **Failed ping НЕ валит whoami** — операторы на flaky network'е получают остальную инфо. - `Region:`, `Default tier:`, `Cluster name:`, `SSH key:` (с маркером `(loaded)` если файл существует, `(missing!)` если path в config'е есть но файла нет на диске, `not set` если не задан). `~/...` tilde-abbreviation через локальный `abbreviate_home` (3 строки, без cross-module surface). - [x] `apprafter auth login/logout/status` — три hidden stub'а per spec §5.7. `Commands::Auth` помечен `#[command(hide = true)]` → не появляется в `apprafter --help` (не загромождает new-user discovery surface). Под-команды реальны (`apprafter auth --help` работает): `login` и `logout` печатают friendly redirect "AppRafter Cloud is not yet available... apprafter target add"; `status` — "self-hosted mode active. Use `apprafter whoami`...". Все три имеют ссылку на `https://apprafter.dev`. `AuthCommand` enum — реальный Subcommand (не stub-string), чтобы future Managed impl заполнял ветки без CLI surface re-shape. - [x] `cli/platform-cli/src/commands/whoami.rs` (~150 LOC + 5 unit-тестов): pure `verified_status(target, no_ping)` + `ssh_key_status(opt)` + `abbreviate_home(p)` helpers; orchestrator `run(no_ping)`. Best-effort ping → не валит whoami. - [x] `cli/platform-cli/src/commands/auth.rs` (~60 LOC): три `run_X` функции через shared `print_redirect` helper. - [x] `cli/platform-cli/src/cli.rs`: `Commands::Whoami { no_ping }` + `Commands::Auth { #[command(hide = true)] action: AuthCommand }` + `AuthCommand { Login, Logout, Status }`. - [x] `cli/platform-cli/src/main.rs`: dispatch обеих новых веток. **Тесты:** 10 integration в `whoami_auth_test.rs` + 5 unit в `whoami.rs`: - `whoami_on_empty_store_prints_onboarding_hint` - `whoami_with_active_target_renders_summary_and_honours_no_ping` — пинит что синтетический токен **никогда** не появляется в stdout (regression-guard на leak). - `whoami_with_real_ping_reports_verified_on_mockito_200` - `whoami_with_real_ping_reports_failure_hint_on_mockito_401` — проверяет что 401 не валит exit code + содержит `--renew` hint. - `whoami_with_real_ping_reports_failure_when_provider_unreachable` — closed-port path. - `auth_login_prints_friendly_redirect_to_target_add` - `auth_logout_prints_friendly_redirect_with_nothing_to_logout_phrasing` - `auth_status_explains_self_hosted_mode_and_points_at_whoami` - `auth_group_is_hidden_from_top_level_help` — `apprafter --help` НЕ содержит `auth`. - `auth_subcommand_help_is_still_reachable` — `apprafter auth --help` работает (hide ≠ delete). - 5 unit: `verified_status` × 2 (no_ping + no_token), `ssh_key_status` × 3 (loaded / missing / not-set). **Acceptance:** - ✅ `apprafter whoami` на TTY/CI без active target → onboarding hint + Identity-line. - ✅ С active target — рендер всех полей + verified status (или skip с `--no-ping`). - ✅ Token никогда не leak'ится в stdout. - ✅ `apprafter auth login/logout/status` печатают friendly redirect + Managed-roadmap URL. - ✅ `apprafter --help` не показывает `auth`; `apprafter auth --help` показывает все три subcommand'а. - ✅ `cargo test --workspace`: 36 (target_test без изменений) + 10 (whoami_auth_test новый) + 120 (cli-providers без изменений) — 0 failures. fmt + clippy + SPDX (155 files) — clean. **Out-of-scope (отложено):** - "Account" / "Last used" / "Cluster: provisioned/not" в whoami — нужны (a) Hetzner endpoint которого нет публично, (b) per-target state cache wire-up через A.8, (c) telemetry on operational commands. Закроется когда A.8 land'нет state-per-target. - Real AppRafter Cloud auth — Managed offering, далеко за пределами M1.5. - ADR `docs/adr/0014-cli-command-structure.md` про резервирование `auth` namespace + resource-first grouping — Track A.12 (final docs+ADR pass). **Зависит от:** 1.66A.5 ✅ (target store + load_target). **Размер:** S (один цикл, ~0.5 рабочего дня). --- ### 1.66A.7 `apprafter doctor` ✅ > v0.1.81 — sub-phase 1.66A.7 shipped: self-diagnostic команда (target checks + env checks + DNS probe; trichotomy PASS/WARN/FAIL; FAIL → exit 1 для CI gates). **Source:** `cli-dx-task.md` §5.9 + §17 row 7. **Поставка:** - [x] `apprafter doctor [--target ] [--no-ping]` — новая top-level команда. - [x] `cli/platform-cli/src/commands/doctor.rs` (~520 LOC + 11 unit-тестов): pure `Check { name, status, detail, hint }` + `DoctorReport { target_name, target_checks, env_checks }` data layer, отдельные `build_target_checks` / `build_env_checks` / `print_report` функции; orchestrator `run` зовёт всё + exit-1 на FAIL. - [x] **Target checks** (когда target resolved): - `Config file readable` — через `load_target`; on `TargetNotFound` сразу FAIL с available-hint'ом. - `Credentials file present (mode 0600)` — на Unix проверяет permissions, WARN если drift'нул от 0600; на других OS просто existence-check. - `Provider \`X\` supported` — whitelist (`hetzner-cloud`). - `Token format valid` — `validate_hetzner_token_format`; FAIL с `--renew` hint'ом если сломан. - `Token verified against provider API` — `HetznerCloudValidator::validate_credentials()` с timing (`{ms} ms`); WARN если `--no-ping` / нет токена; FAIL с разделением 401 (token rejected → `--renew` hint) / non-401 HTTP / transport. - `SSH key readable` — exists + read_to_string + parse algo из OpenSSH-первой строки; FAIL если path в config'е есть но файла нет на диске (stale config); WARN если path не задан. - [x] **Env checks** (всегда): - `\`kubectl\` on PATH` / `\`helm\` on PATH` / `\`ssh\` on PATH` — `Command::new(tool).args(...).output()`, PASS с первой непустой строкой (stdout ИЛИ stderr — `ssh -V` пишет в stderr), WARN с hint'ом если binary не найден. Лояльно — отсутствие optional-tool не валит doctor. - `DNS resolves \`api.hetzner.cloud\`` — `ToSocketAddrs::to_socket_addrs("host:443")`; PASS с `443/tcp` detail или FAIL с resolver-error hint'ом. - [x] **Rendering**: ` ✓ name (detail)` / ` ⚠ name (detail)` / ` ✗ name (detail)` + ` hint: ` на отдельной indented строке. Trailing summary с разными формулировками для clean / warning-only / FAIL'ed runs. - [x] **Exit policy**: FAIL anywhere → `std::process::exit(1)`; WARN-only → exit 0 + "Ready to go; review warnings". **Тесты (17 новых):** - 11 unit в `commands::doctor::tests`: - `check_status_glyph_renders_distinctly` - `report_counters_split_by_status` - `report_has_failures_returns_false_when_only_warns` - `check_dns_resolves_localhost_passes` (RFC reserved 127.0.0.1) - `check_dns_resolves_invalid_tld_fails` (RFC 6761 `.invalid`) - `check_tool_warns_on_missing_binary` (no `apprafter-doctor-no-such-binary` on $PATH) - `check_provider_known_fails_for_unknown_provider` / `_passes_for_hetzner` - `check_token_format_passes_canonical_token` / `_fails_on_missing_token` - `check_token_ping_warns_when_no_ping_flag_set` - 6 integration в `tests/doctor_test.rs`: - `doctor_on_empty_store_errors_with_onboarding_hint` - `doctor_renders_target_and_env_checks_with_summary` (Target/Env разделы, --no-ping → WARN на ping, summary mentions target name) - `doctor_target_flag_inspects_non_active_target` (`--target secondary`) - `doctor_ssh_key_missing_path_fails_the_run_with_exit_1` (configure ssh-key path → удалить файл → FAIL + exit 1) - `doctor_target_not_found_fails_with_available_hint` (`--target ghost`) - `doctor_summary_line_phrases_outcomes_clearly` (warning-only run → "warning(s)", no FAIL в выводе) **Acceptance:** - ✅ `apprafter doctor` на empty store → typed error + onboarding hint. - ✅ С active target — все 10 (~6 target + ~4 env) checks отрисованы; summary с count'ом PASS/WARN/FAIL. - ✅ `--target ` инспектирует non-active target. - ✅ `--no-ping` → token-ping check как WARN с "skipped — --no-ping". - ✅ Любая FAIL → exit 1 (для CI gates). - ✅ Никаких token leaks в output. - ✅ `cargo test --workspace`: 36 target_test + 10 whoami_auth_test + 6 doctor_test + 120 cli-providers + ... — 0 failures. - ✅ fmt + clippy (-D warnings) + SPDX (158 files) clean. **Out-of-scope (отложено):** - "Region in known list" check — нужен hardcoded list (brittle) или API call (уже есть в ping). Implicit: если ping проходит с этим region'ом, он валиден. - "No active cluster" check — нужен cli-state cross-ref per target. Track A.8. - Color output для PASS/WARN/FAIL — Track A.11 (color/NO_COLOR). - miette-стиль diagnostics — Track A.10. **Зависит от:** 1.66A.4a ✅ (validator), 1.66A.5 ✅ (CRUD load_target). **Размер:** M (один цикл, ~1 рабочий день). --- ### 1.66A.8 Wire `apply` / `destroy` / `import` в target resolution ✅ > v0.1.82 — sub-phase 1.66A.8 shipped: credential resolution chain (`--flag > env > target store`) реально подключена к operational commands. После v0.1.82 — `apprafter target use prod && apprafter apply` без `HCLOUD_TOKEN=...` действительно работает. **Source:** `cli-dx-task.md` §5.10 + §7 + §17 row 8. **Поставка:** - [x] `cli/cli-core/src/credentials.rs` — новый модуль: - `resolve_hetzner_token(cli_flag, paths, target_override) -> Result` — implements 3-step chain. cli_flag (highest) > `HCLOUD_TOKEN` env > active target's credentials.yaml (или `--target ` override). - `resolve_hetzner_ssh_public_key(paths, target_override) -> Result>` — analogous chain для SSH public key BODY. Env `APPRAFTER_SSH_PUBLIC_KEY` > target store path → read file. - `read_ssh_public_key_body(path)` pure helper. - Constants `HCLOUD_TOKEN_ENV` / `SSH_PUBLIC_KEY_ENV` для shared use. - Error messages enumerate **все 3** пути (flag / env / `apprafter target add`) чтобы оператор сразу видел альтернативы. - [x] cli-core re-export через `pub use credentials::*`. - [x] `cli_core::TEST_ENV_MUTEX: pub(crate) static Mutex<()>` в `lib.rs` (cfg(test)-gated) — serialises env-touching unit tests across modules (target.rs + credentials.rs обе flip'ают HCLOUD_TOKEN / CONFIG_DIR_ENV, race без shared mutex). - [x] `commands/apply.rs::run(target_override: Option<&str>)` — заменил direct `env::var("HCLOUD_TOKEN")` на `resolve_hetzner_token(None, &target_store, target_override)`. `build_ssh_specs` теперь thread'ит target_store + target_override и вызывает `resolve_hetzner_ssh_public_key`; manifest `sshKeys` block по-прежнему wins (highest precedence на той ветке). - [x] `commands/destroy.rs::run(yes, target_override)` — analogous wiring. Empty-state early-exit моложе credential resolution чтобы `destroy --yes` в no-Hetzner-state директории не падал на missing-creds. - [x] `commands/import.rs::run(force, dry_run, target_override)` — analogous. - [x] `cli.rs` — новый `--target ` flag на `Apply` / `Destroy` / `Import`. - [x] `main.rs` dispatch обновлён. **Тесты (16 новых cli-core unit + 1 integration smoke):** - 16 в `cli_core::credentials::tests`: - CLI flag wins over env + store - env wins over store when no flag - store fallback when flag + env absent - `--target ` override picks named target not active - error with 3-paths hint when nothing configured - error when target exists but no token stored - error with override for missing target surfaces "available" hint - SSH key env wins over target path - SSH key reads target path when env absent (with trim) - SSH key returns None when nothing configured - SSH key errors loudly on unreadable path - 1 в `tests/cli_smoke.rs` integration: - `apply_target_flag_routes_resolution_at_named_target_and_surfaces_not_found` — seed target store с `real`, run `apply --target ghost` → typed error содержит `ghost`, `not found`, `real` (available hint). - 3 prior `apply_without_token_*` / `import_without_token_*` integration тесты обновлены: добавлен `APPRAFTER_CONFIG_DIR=` для изоляции от user's real `~/.config/apprafter/`; assertions enumerate новые "3-paths" error message tokens (`--token`, `HCLOUD_TOKEN`, `apprafter target add`). **Acceptance:** - ✅ `apprafter apply` без `HCLOUD_TOKEN` env читает токен из active target — главная цель Track A. - ✅ `--target ` override per-invocation, без switching active. - ✅ Existing CI scripts (`HCLOUD_TOKEN=... apprafter apply`) работают без изменений — backwards-compat preserved (env остаётся step 2 в chain). - ✅ Empty-store + no env + no flag → typed error с **всеми 3** путями выхода в сообщении. - ✅ Stale `--target ` → "did you mean..." hint via canonical TargetNotFound. - ✅ `cargo test --workspace`: 90 cli-core (+16 credentials) + 47 cli_smoke (+1 new integration, 3 prior updated) + 42 target_test + 10 whoami_auth_test + 6 doctor_test + ... — 0 failures. - ✅ fmt + clippy (-D warnings) + SPDX (160 files) clean. **Out-of-scope (отложено):** - `--token ` flag на `apply`/`destroy`/`import` (secrets в shell history — wait until A.10 miette pass решает UX). - Migration `/.apprafter/state.json` → per-target `state//state.json` — отдельная iteration после bootstrap-all. - `kubeconfig` / `argocd-password` / `cluster-bootstrap` — не используют HCLOUD_TOKEN напрямую (работают на kubeconfig); скип. - `init` — не нужны creds (stub-like, write state.json only). **Зависит от:** 1.66A.5 ✅ (target store CRUD). **Размер:** M (один цикл, ~1.5 рабочих дня). --- ### 1.66A.9 `apprafter bootstrap-all` orchestrator ✅ > v0.1.84 — initial landing: 3-phase wrapper `apply` → kubeconfig-poll → `cluster-bootstrap` под единым `indicatif::MultiProgress` UX, `--dry-run` со списком subcommand-команд. > v0.1.85 — hotfix UX после ручного walk'а v0.1.84: `MultiProgress` рендерил finished spinner'ы поверх каждого helm/kubectl `println` (10+ дублированных строк); spinner Phase 1/3 fought с tracing-логами apply/cluster-bootstrap за тот же row; dry-run печатал `` placeholder вместо реального имени активного target'а и не давал понять что произойдёт за каждой фазой. v0.1.85 переезжает на single-bar-per-phase pattern, Phase 1/3 без spinner'а (только `→ start` / `✓ end` static-строки), Phase 2 keeps spinner because retry loop owns all output. Dry-run resolves active target name + грузит config.yaml и расписывает фазы человеческим языком. **Source:** `cli-dx-task.md` §5.11 + §17 row 9. **Поставка:** - [x] `cli/cli-core/Cargo.toml` workspace dep `indicatif = "0.17"`; `platform-cli/Cargo.toml` direct dep. - [x] `commands/kubeconfig.rs` рефакторинг: новый `pub fn fetch_and_cache(refresh, target_override) -> Result` возвращает YAML без `print!`, прежний `run` стал thin wrapper. Внутри теперь `resolve_hetzner_token` (cli-dx-task.md §7) вместо прямого `env::var("HCLOUD_TOKEN")` — Phase 2 поллинг подхватывает active target's токен идентично `apply`. - [x] `cli.rs` — новый `Commands::Kubeconfig { refresh, target }` (`--target` override credential resolution chain) + `Commands::BootstrapAll { target, dry_run }`. - [x] `main.rs` dispatch обновлён. - [x] `commands/bootstrap_all.rs` (v0.1.85 UX layout): - Phase 1/3 — `apply::run(target_override)` без spinner'а: `→ [1/3] apply provisioning…` перед вызовом, `✓ [1/3] apply done in Ns` после. Apply сам логирует через `tracing` на stderr — spinner вокруг него только конкурировал бы за тот же ряд терминала и оставлял stale-кадры после каждого `helm`/`kubectl` write. - Phase 2/3 — retry-loop `kubeconfig::fetch_and_cache(true, target_override)` каждые 10s до 5 минут (`KUBECONFIG_POLL_TIMEOUT = 300s`, `KUBECONFIG_POLL_INTERVAL = 10s`). Здесь spinner оправдан: цикл наш, никаких inner subcommand'ов не пишут в stdout, message обновляется с attempt counter + truncated last error. Завершается `finish_and_clear()` + static success line. - Phase 3/3 — `cluster_bootstrap::run()` без spinner'а (та же логика, что Phase 1). - `--dry-run` short-circuits BEFORE any side-effect — load `default_config_root()` + `resolve_active_target_name()` + `load_active_target_config()`, печатает реальное имя active target (или `--target` override label), provider/region/tier/cluster/ssh-key из `config.yaml`, и human-readable описание каждой фазы (что именно она делает, не просто «вызовет такую-то subcommand»). - `failed(num, name, elapsed, err)` helper — на error печатает `✗ [N/3] phase FAILED after Ns` в stderr и пробрасывает CliError неизменно (timing accountable без потери error chain). - Финал: `bootstrap-all complete in Tm00s (apply X + kubeconfig Y + bootstrap Z)` — single-line breakdown total + per-phase. - [x] `commands/mod.rs` регистрирует новый модуль. **Тесты (4 unit + 6 integration):** - 4 в `commands::bootstrap_all::tests`: - `format_elapsed_uses_seconds_under_one_minute` - `format_elapsed_switches_to_minutes_at_sixty_seconds` - `short_error_keeps_first_line_only` - `short_error_truncates_long_first_line_with_ellipsis` - 6 в `tests/bootstrap_all_test.rs`: - `bootstrap_all_dry_run_prints_three_phase_plan_without_provider_calls` — fresh store / no token / no base-URL → success + all 3 phase labels + `Phases:` block в stdout. - `bootstrap_all_dry_run_with_empty_store_prints_onboarding_hint` — empty target store → `no active target` + `apprafter target add` hint. - `bootstrap_all_dry_run_with_target_override_labels_it_clearly` — `--target work` → `Target: work` + `via --target override` label. - `bootstrap_all_dry_run_with_active_target_resolves_name_and_config` — seed real target через `target add`, dry-run resolves `Target: myprod (active)` + Provider/Region/Tier из `config.yaml`. - `bootstrap_all_help_documents_dry_run_and_target_flags` — `--help` mentions both flags. - `bootstrap_all_rejects_unknown_flag` — clap surface contract guard. **Acceptance:** - ✅ `apprafter bootstrap-all --dry-run` exits 0 на любой директории / любом credential state, никаких provider calls. - ✅ Dry-run показывает реальное имя active target + полный target config (не placeholder ``). - ✅ `--target ` override доходит и до apply, и до Phase 2 kubeconfig poll (single resolution path). - ✅ Real run на свежем Hetzner токене даёт **clean** vertical output: `→ start / inner output / ✓ end` без дублирования спиннер-строк. - ✅ `cargo test --workspace` — 542 tests, 0 failures. - ✅ fmt + clippy (-D warnings) + SPDX (161 файл) clean. **Out-of-scope (отложено):** - Capturing inner helm/kubectl output to a buffer (показывать только on failure) — feasible but invasive; пользователю всё ещё нужно видеть прогресс helm install. Доработка цвета + табличного hiding — Track A.11. - Idempotent re-run / skip-already-installed semantics — Argo CD handle'ит это в Phase 3 через `helm upgrade --install`; Phase 1 — Hetzner labels; Phase 2 — `--refresh` always re-fetches. - miette-styled error rendering при timeout — Track A.10. - **Phase 2 polish (отложено до отдельной итерации, ~A.9c)** — две связанные доработки, замеченные на ручном walk'е v0.1.85: (a) Phase 2 стабильно завершается за `1m00s` потому что `ssh` упирается в kernel TCP connect timeout (~30s) пока cloud-init поднимает sshd; нужен `ConnectTimeout=5` в SSH wrapper'е → attempt 1 fail'ится за 5s вместо 30s, total Phase 2 падает до ~20-30s, attempts равномернее. (b) Label `[2/3] kubeconfig` вводит в заблуждение — реально это время полного boot'а ноды (cloud-init + k3s startup), kubeconfig fetch — копеешный финальный шаг. Переименовать на `[2/3] k3s-ready` / `[2/3] cluster-up` / подобное; success-строка станет `up in Ns` вместо `ready in Ns`. dry-run phase block обновить синхронно. **Зависит от:** 1.66A.8 ✅ (credential resolution chain — Phase 2 needs `resolve_hetzner_token`). **Размер:** S (один цикл, ~0.5 рабочего дня). --- ### 1.66A.10 miette diagnostic refinement ✅ > v0.1.86 — sub-phase 1.66A.10 shipped: каждый user-facing вариант `CliError` теперь несёт стабильный `code(apprafter::*)` + многострочный `help(...)` через `miette::Diagnostic` derive, а binary entry point рендерит через `miette::MietteHandlerOpts` (`fancy` reporter) вместо `color-eyre`. Результат — rustc-quality error UX: `error:` + код, бокс-обёрнутое сообщение, многострочный `help:` с конкретными next-step командами. Зависимость `color-eyre` удалена. **Source:** `cli-dx-task.md` §10 + §17 row 10. **Поставка:** - [x] `cli/Cargo.toml` workspace deps: `miette = { version = "7", features = ["fancy"] }`; удалена `color-eyre` (больше не используется). - [x] `cli/cli-core/Cargo.toml` — `miette` direct dep (поскольку `CliError` derives `Diagnostic` в cli-core). - [x] `cli/platform-cli/Cargo.toml` — `miette` direct dep, удалена `color-eyre`. - [x] `cli/cli-core/src/error.rs`: - `CliError` теперь derives `miette::Diagnostic` рядом с `thiserror::Error`. - 9 вариантов получили `#[diagnostic(code(...), help(...))]`: - `CueNotFound` — `apprafter::env::cue_not_found` + nix-develop hint. - `CueExport` — `apprafter::env::cue_export_failed` + `cue vet` reproduce hint. - `Hetzner` — `apprafter::provider::hetzner_api_error` + enumerate 401/403/429/5xx common causes + `apprafter doctor` next-step. - `ServerTypeUnavailable` — `apprafter::provider::server_type_unavailable` + cx22→cpx22 retirement story. - `InvalidState` — `apprafter::state::corrupt` + `apprafter import` recovery hint. - `InvalidTargetConfig` — `apprafter::target::invalid_config` + per-target dir recovery path. - `TargetNotFound` — `apprafter::target::not_found` + `target list` + `target add` hints. - `Io` / `Json` / `Yaml` — каждая получает `apprafter::io::*` code + variant-specific help. - `Other` (catch-all) — `apprafter::cli::other` со стабильным code чтобы log-analytics могла find'ить recurring messages кандидатами на promotion в typed variant. - File-level `#![allow(unused_assignments)]` для подавления `miette-derive` 7.6.0's generated reassignments (lint fires на generated code за нашим контролем; локальное `#[allow]` на enum не пропускается через derive macro). - [x] `cli/platform-cli/src/main.rs`: - Return type `color_eyre::Result<()>` → `miette::Result<()>`. - `color_eyre::install()` заменён на `miette::set_hook(...)` с `MietteHandlerOpts::new().terminal_links(true).unicode(true).context_lines(2).with_cause_chain().build()`. - Вынесен `fn dispatch(args: Cli) -> cli_core::Result<()>` — типизированный CliError→miette::Report happens exactly once на binary boundary, inner code keeps original `?` ergonomics over `cli_core::Result`. - [x] doc-comment на `cli-core::error` объясняет policy: новые call-sites должны добавлять typed variants с кодами вместо `Other(format!(...))`. **Тесты (8 unit + 3 integration):** - 8 в `cli_core::error::tests`: - `target_not_found_diagnostic_carries_stable_code_and_helpful_hint` — code = `apprafter::target::not_found`, help содержит `target list` + `target add`. - `invalid_target_config_diagnostic_points_at_target_directory` — code = `apprafter::target::invalid_config`, help содержит `$XDG_CONFIG_HOME/apprafter/targets/` + `target add`. - `hetzner_diagnostic_help_enumerates_401_403_429_5xx` — help раскрывает все 4 типа failures + `target add` + `doctor`. - `server_type_unavailable_diagnostic_explains_retirement_path` — help упоминает cx22 + cpx22 retirement. - `cue_not_found_diagnostic_recommends_nix_develop` — help содержит `nix develop` + `docs/contributing/setup.md`. - `invalid_state_diagnostic_recommends_import_for_recovery` — help содержит `apprafter import`. - `io_error_passes_through_with_dedicated_code` — code = `apprafter::io::error`, wrapped OS message survives в Display. - `other_keeps_catch_all_code_so_recurring_variants_can_be_filtered` — code = `apprafter::cli::other` (stable для log analytics). - 3 в `tests/miette_render_test.rs` (полноценный subprocess-based render contract): - `unhandled_error_renders_with_miette_help_line` — `apply` без creds → stderr содержит `help:` + `apprafter::cli::other` code (catch-all variant goes through fancy renderer). - `typed_target_not_found_renders_with_dedicated_code_and_help` — `target show ghost` → stderr содержит `apprafter::target::not_found` + `help:` + `apprafter target list` + `apprafter target add` substrings из help text. - `no_color_env_yields_ansi_free_stderr` — `NO_COLOR=1` → no `\x1b` bytes в stderr но `help:` + diagnostic code still present (pipe-friendly). **Acceptance:** - ✅ Любой `CliError` reaching `main` рендерится с `Error: apprafter::<...>` + box-wrapped message + `help:` block (NOT с `Debug` stringification). - ✅ Stable diagnostic codes per variant для log-analytics + future error catalogue. - ✅ `NO_COLOR=1` респектится (no ANSI sequences в stderr). - ✅ `cargo test --workspace` — 553 tests, 0 failures (+11 over v0.1.85's 542). - ✅ fmt + clippy (-D warnings) + SPDX (163 файла) clean. - ✅ `color-eyre` workspace + platform-cli deps удалены. **Out-of-scope (отложено):** - `#[source_code]` + `#[label]` span highlighting per variant (например, `InvalidHetznerTokenFormat` с подсветкой именно префикса) — feature exists в miette, но требует carrying source text через error chain. Promote later when CUE manifest parsing errors get the same treatment. - Promotion массовых `CliError::Other(format!(...))` call sites в типизированные варианты — `Other` остаётся catch-all со стабильным code; конвертация — отдельная работа, по мере того как specific shapes повторяются. - Cause-chain rendering refinements (multi-level nested errors) — `with_cause_chain()` уже включён в hook builder, но AppRafter ещё не порождает глубоких цепочек. Полировка — when needed. **Зависит от:** 1.66A.9 ✅ (нужен `bootstrap-all` рабочий путь для smoke tests миette-рендера). **Размер:** S (один цикл, ~0.5 рабочего дня). --- ### 1.66A.11 Aliases + semantic colors + NO_COLOR ✅ > v0.1.88 — sub-phase 1.66A.11 shipped: новый `cli_core::style` модуль с семантическими хелперами поверх `owo-colors` (auto-honours `NO_COLOR` через `supports-colors` feature); цвет applied на `bootstrap-all` markers (`→` cyan, `✓` green, `✗` red) + `doctor` PASS/WARN/FAIL glyphs (green/yellow/red); subcommand aliases — `target list/show/remove` ↔ `ls`/`info`/`rm`, `kubeconfig` ↔ `kc`, `cluster-bootstrap` ↔ `cb`, `bootstrap-all` ↔ `up`. Уже существовавший `target` ↔ `t` сохраняется, новые aliases прицепляются к нему (`apprafter t ls`). **Source:** `cli-dx-task.md` §17 row 11. **Поставка:** - [x] `cli/Cargo.toml` workspace dep `owo-colors = { version = "4", features = ["supports-colors"] }`; `cli-core/Cargo.toml` direct dep. - [x] `cli/cli-core/src/style.rs` — новый модуль: - `ok(t)` — green (PASS / `✓` / verified). `Stream::Stdout` для авто-NO_COLOR. - `warn(t)` — yellow (WARN / soft failures). - `fail(t)` — red. `Stream::Stderr` — callsites that consume `fail()` write to stderr. - `info(t)` — cyan (phase markers `→`, column headers, `(active)` tags). - `dim(t)` — dimmed (tertiary annotations типа `(unset — apply uses platform-1)`). - `bold(t)` — bold emphasis (target names, cluster names). Combine: `info(&bold("dev"))`. - Все возвращают `String` (упрощено — `if_supports_color` возвращает hard-to-name generic type; форматирование в строку pragmatic и pollutes только небольшие call sites). - [x] `cli-core/src/lib.rs` — `pub mod style;`. - [x] `commands/bootstrap_all.rs`: - Phase markers `→`/`✓`/`✗` через `style::info/ok/fail`. - Phase 2 spinner success line использует `style::ok`. - Phase failure marker через `style::fail`. - [x] `commands/doctor.rs`: - Новый `CheckStatus::coloured_glyph(&self) -> String` — green ✓ / yellow ⚠ / red ✗. - `print_check_line` использует coloured glyph. - [x] `cli/platform-cli/src/cli.rs` — aliases: - `Kubeconfig` — `alias = "kc"`. - `ClusterBootstrap` — `alias = "cb"`. - `BootstrapAll` — `alias = "up"`. - `TargetCommand::List` — `alias = "ls"`. - `TargetCommand::Show` — `alias = "info"`. - `TargetCommand::Remove` — `alias = "rm"`. **Тесты (2 unit + 7 integration):** - 2 в `cli_core::style::tests`: - `ok_returns_ansi_free_text_when_stream_is_not_a_tty` — под `cargo test` stdout не TTY → no ANSI bytes, literal text survives. - `warn_fail_info_dim_bold_all_round_trip_text_under_no_tty` — same contract для всех 5 helpers. - 7 в `tests/aliases_test.rs`: - `target_ls_alias_routes_to_target_list` — sub-process сравнение stdout/exit между `target list` и `target ls` (identical bytes). - `target_rm_alias_routes_to_target_remove` — `rm ghost --yes` → typed `apprafter::target::not_found`. - `target_info_alias_routes_to_target_show` — same not-found surface. - `kc_alias_routes_to_kubeconfig` — surfaces "no hetzner_cloud state" hint identically. - `cb_alias_routes_to_cluster_bootstrap` — same. - `up_alias_routes_to_bootstrap_all_dry_run` — `up --dry-run` exits 0 + prints `DRY RUN` plan identical to `bootstrap-all --dry-run`. - `t_alias_for_target_still_works_alongside_new_alias_chain` — `apprafter t ls` chains `t` (target) ↔ `ls` (list) → empty-store onboarding hint surfaces. Pins muscle-memory kubectl-style path. **Acceptance:** - ✅ `bootstrap-all` real run в TTY показывает coloured phase markers (green ✓ / cyan →). - ✅ `doctor` PASS rows green, WARN rows yellow, FAIL rows red. - ✅ `NO_COLOR=1` или non-TTY pipe → output identical to monochrome v0.1.87 (zero ANSI bytes). - ✅ Все 6 новых aliases работают через subprocess: `apprafter ls`/`info`/`rm`/`kc`/`cb`/`up` + chained `t ls`/`t info`/`t rm`. - ✅ `cargo test --workspace` — 564 tests, 0 failures. - ✅ fmt + clippy (-D warnings) + SPDX (165 файлов) clean. **Out-of-scope (отложено):** - Цвет на `target list` table (через `tabled` cell styling) — feasible but требует custom cell renderer; на стандартных терминалах current monospace table читается хорошо. Promote позже если walk feedback потребует. - Цветная identity-строка в `whoami` (target name + cluster bold-cyan) — следующее iterative refinement; foundation готов через `style::bold` + `style::info`. - `style::ok_strong` / `style::fail_strong` background variants — добавим если нужно различать "ready" vs "ready + critical path". **Зависит от:** 1.66A.10 ✅ (miette уже использует свой палитру; `style` модуль координирует семантику чтобы наш output совпадал с miette's по тонам — green/yellow/red). **Размер:** S (один цикл, ~0.5 рабочего дня). --- ### 1.66A.12 Docs + ADR ✅ > v0.1.90 — sub-phase 1.66A.12 shipped: финальная подфаза Track A. Документация для операторов переписана под пост-Track-A flow (`apprafter target add` + `apprafter up` вместо env-var + `cargo run`), credential resolution chain и target store layout вынесены в reference, диагностические коды каталогизированы, full CLI reference добавлен в `docs/reference/cli.md`, дизайн-решения Track A закрыты ADR 0030. mkdocs nav обновлён. Track A теперь закрыт — открывается Track B (M1.5 1.66 platform-stack rethink). **Source:** `cli-dx-task.md` §17 row 12. **Поставка:** - [x] `docs/adr/0030-cli-target-store-and-credential-chain.md` — новый ADR, кодифицирует 4 design decisions: D1 target store (file layout + `APPRAFTER_CONFIG_DIR` override + per-target dirs + mode 0600), D2 three-step credential resolution chain (flag → env → store, including `--target` override), D3 `miette` для user-facing diagnostics (stable `apprafter::::` codes + multi-line `help` + `#[diagnostic_source]` cause chains), D4 subcommand aliases + semantic colour palette. Включает 6 alternatives considered, 4 risks с mitigations, re-evaluation triggers (AWS landing, Phase 2 opening, credential leak). - [x] `docs/operator-guide/quickstart.md` — полностью переписан. Old flow (`export HCLOUD_TOKEN` + `cargo run --bin apprafter -- init`) → new flow (`apprafter target add prod ...` + `apprafter bootstrap-all`). Объяснены 3-phase wrapper, dry-run preview, per-phase recovery, doctor self-check, aliases (kc/cb/up/t ls/...), миette error reading. Подробный day-2 ops table + Application CRD usage. - [x] `docs/operator-guide/target-store.md` — новая страница. File layout reference (`config.yaml` + per-target dirs + `auth/` + `state/`), field reference table для `TargetConfig`, credential resolution chain explained, 4 common patterns (single-cluster / multi-cluster / CI-env-only / token rotation), 3 anti-patterns. - [x] `docs/operator-guide/troubleshooting.md` — новая страница. Diagnostic-code catalogue: каждый из 11 кодов получает 2-3 параграфа объяснения (`env::cue_not_found`, `env::cue_export_failed`, `provider::hetzner_api_error`, `provider::server_type_unavailable`, `state::corrupt`, `target::invalid_config`, `target::not_found`, `target::token_rejected`, `target::provider_unreachable`, `io::error/json/yaml`, `cli::other`). + walk-found common failures section + worked example reading the layered cause chain. - [x] `docs/reference/cli.md` — новая страница. Top-level binary surface, global env vars table, every subcommand documented (target/whoami/doctor/init/apply/destroy/import/kubeconfig/cluster-bootstrap/argocd-password/bootstrap-all/status/login/upgrade-tier/auth), full aliases reference table. - [x] `docs/operator-guide/index.md` обновлён — links to new pages, Track A status note (closed), no more "stub" wording. - [x] `docs/reference/index.md` обновлён — CLI reference из stub стал first-class, диагностические коды cross-link'нуты на troubleshooting page. - [x] `mkdocs.yml` nav — Operator Guide + Reference раскрываются в nested entries (quickstart / target store / gitops walk / troubleshooting / recovery; reference index + CLI page). **Тесты:** docs-only changes, никаких runtime tests. SPDX gates: 166 файлов pass. fmt + clippy: clean. Workspace tests: 564 passed (unchanged). mkdocs `--strict` build не запустился локально из-за `nix shell` env quirk (mkdocs binary не видит mkdocs-material theme); CI workflow `.github/workflows/docs.yml` валидирует. **Acceptance:** - ✅ ADR 0030 покрывает все 4 design decisions с rationale, alternatives, risks, re-evaluation triggers. - ✅ Operator quickstart описывает post-Track-A flow без legacy `cargo run` / env-var-only префиксов. - ✅ Target store layout + credential chain документированы в одной reference page. - ✅ Все 11 diagnostic codes имеют next-step CLI команды в troubleshooting page. - ✅ Full CLI reference covers все 13 subcommand'ов + 6 aliases. - ✅ mkdocs nav surface'ит новые страницы. - ✅ SPDX clean (166 файлов). **Out-of-scope (отложено):** - Auto-generated CRD field reference — Phase 8.1 target per `docs/reference/index.md`. - Mass-rewrite of `docs/dev-guide/quickstart.md` — Track A не трогал developer flow напрямую, отдельная итерация когда developer experience станет приоритетом. - Translations — все docs остаются English-only. **Зависит от:** 1.66A.11 ✅ (последний код-меняющий sub-phase Track A — нужны все имплементированные фичи чтобы корректно их задокументировать). **Размер:** M (один цикл, ~1 рабочий день — 4 новых doc-файла + 1 ADR + 2 update'а + nav). --- ### Track A backlog (закрыт в v0.1.91) Originally items surfaced during Track A walks. Cleared: 1. **Phase 2 polish (A.9c)** ✅ — v0.1.91 закрыл оба: - SSH `ConnectTimeout=5` добавлен в `SshKubeconfigFetcher::build_command` — первая attempt fail'ится за 5s вместо ~30s на kernel TCP timeout. Typical Phase 2 на cpx22 + Ubuntu 24.04 падает с ~60s до ~20-40s. - `[2/3] kubeconfig` rename → `[2/3] k3s-ready`: спиннер label, success line, failed() marker, dry-run plan, total summary breakdown — все consistent. Сообщение "waiting for cloud-init + k3s on the new node…" честно отражает что мы реально делаем. +1 regression test (`ssh_fetcher_caps_connect_timeout_at_five_seconds`); existing dry-run integration test обновлён под новый label. Docs (quickstart, troubleshooting, cli reference) тоже подтянуты. После v0.1.91 открывается **Track B (M1.5 sub-phase 1.66 platform-stack rethink)** — главная архитектурная работа M1.5. --- ### 1.66 platform-stack monorepo skeleton + CUE source layout ✅ > v0.1.92 — sub-phase 1.66 shipped: top-level `platform-stack/` со всем CUE-only source per ADR 0028. **Layout flat** — `platform-stack/cue/` единая директория с filename prefixes (`component_.cue`, `tier_.cue`) вместо subdirectory groupings. Subdirs пробовали — CUE считает их отдельными package instances even when `package` declaration matches, что ломает cross-file `_components` merging. Дополнительные design-walk gotchas (autobinding-strip + typed `_components` strip + `vet -c` rejecting defaults) задокументированы в `platform-stack/README.md` + комментах в `platform.cue`. **Source:** ADR 0028. **Цель:** заложить структуру `platform-stack/` в монорепо. CUE source-of-truth для всех Argo CD Application определений платформенных компонент. **Поставка:** - [x] New top-level subdir `platform-stack/` (path указан с префиксом `apprafter/` в исходном тексте — это GitHub URL convention `apprafter/apprafter` org+repo; в clonированном дереве это просто `platform-stack/` на root уровне рядом с `cli/`, `operator/`, `schemas/`). - [x] `cue/platform.cue` — umbrella schema (`#Version` / `#Channel` / `#Tier` / `#ComponentSource` / `#Component` / `#ComponentSet` / `#PlatformValues` / `_components: {}` package-level base). - [x] `cue/component_cilium.cue` — Cilium 1.16.5, kube-proxy replacement, IPAM kubernetes, Hubble off by default. - [x] `cue/component_cert-manager.cue` — jetstack v1.16.2 + CRDs enabled. - [x] `cue/component_argocd.cue` — Argo CD 7.7.7 self-managing (`prune: false`), Dex off. - [x] `cue/component_apprafter-operator.cue` — pinned to v0.1.91 (Track A closing tag) from `oci://ghcr.io/apprafter/apprafter-operator`. - [x] `cue/component_admission-webhook.cue` — pinned to v0.1.91, 2 replicas default. - [x] `cue/component_backstage.cue` — Git-source manifests, conditional on `values.backstage.domain`, default-off в tier-1 overlay. - [x] `cue/component_network-policies.cue` — default-deny + DNS + Argo-CD egress allowance bundle. - [x] `cue/component_argocd-cue-cmp.cue` — declared but `enabled: false` by default до sidecar wiring step в 1.69. - [x] `cue/tier_solo.cue` — tier 1 overlay (single cpx22, Hubble off, Backstage off, argocd-cue-cmp off, single-replica everything). - [x] `cue/tier_team.cue` — tier 2 overlay (Hubble on relay+UI, Backstage on, admission-webhook + cert-manager + operator at 2 replicas). - [x] `cue/compatibility.cue` — `#ChangeClass` + `#VersionRecord` schema + initial 0.2.0 entry classified `safe` (no behaviour change vs in-tree v0.1.x bootstrap). - [x] `platform-stack/Chart.yaml.tmpl` — template для umbrella chart metadata (рендерится в `dist//Chart.yaml` через `cue cmd render` в 1.67). - [x] `platform-stack/README.md` — full layout + contribution model + distribution + forking story + design-walk decision rationale. - [x] `platform-stack/CHANGELOG.md` — initial planned 0.2.0 entry. - [x] `scripts/lint-cue.sh` расширен: `cue fmt --check` + `cue vet` теперь покрывают `./platform-stack/cue/...`. - [x] `scripts/check-spdx-headers.sh` patterns добавлены: `platform-stack/cue/**/*.cue` + `platform-stack/Chart.yaml.tmpl`. SPDX gate теперь 167 файлов. **Тесты:** scaffold-only release — никаких runtime tests, валидируем CUE-слой: - [x] `bash scripts/lint-cue.sh` — `cue fmt --check` + `cue vet` clean. - [x] `nix run nixpkgs#cue -- vet -c ./platform-stack/cue/...` — strict concreteness check passes (exit 0). - [x] `nix run nixpkgs#cue -- eval ./platform-stack/cue/... -e tier1` / `-e tier2` — рендерят fully-concrete components map. **Acceptance:** - ✅ `cue vet -c ./platform-stack/cue/...` exits 0; все schemas валидны + полностью concrete на обоих tiers. - ✅ Все 8 компонентов declared в CUE. Hardcoded values в `cli-providers::k8s::*` остаются на месте до migration в 1.71 — `platform-stack/` сейчас параллельная source-of-truth, не replacement. - ✅ README ясно описывает: CUE only в Git, rendered chart живёт в OCI (`oci://ghcr.io/apprafter/platform-stack:`), GitHub Release `.tgz` secondary. **Design-walk gotchas** (зафиксированы в README + komments в `platform.cue` чтобы будущие contributors не переоткрывали): - Subdirectory split → CUE считает sibling dirs отдельными package instances, `_components` не мерджились. **Fix:** flat `cue/` с filename prefixes. - `#ComponentSet` autobinding `[NAME=string]: #Component & { name: NAME }` re-применяет `#Component` на каждом overlay unification и стрипит concrete fields. **Fix:** plain `[string]: #Component` + explicit `name:` per component. - Typed `_components: #ComponentSet` — та же проблема. **Fix:** plain `_components: {}` с локальной type-conformance на declaration site. - CUE 0.10+ `vet -c` flags `bool | *false` как incomplete даже когда default applies. **Fix:** explicit pin per tier (`tier_team.cue`'s `argocd-cue-cmp.enabled: false`). **Out-of-scope (отложено):** - `dist/` renderer + `templates/applications.yaml` template + `make render` — sub-phase 1.67. - Publish workflow (`.github/workflows/platform-stack-publish.yml`) — 1.68. - Argo CD CMP sidecar wiring — 1.69. - Migration `cli-providers::k8s` hardcoded values → CUE — 1.71. **Зависит от:** — **Размер:** M (один цикл, ~0.5-1 рабочий день; основное время — design-walk на CUE gotchas). --- ### 1.67 `cue cmd render` pipeline + umbrella chart generation ✅ > v0.1.93 — sub-phase 1.67 shipped: CUE-tools-based renderer + Makefile + per-tier examples + Helm-native values schema. `make -C platform-stack render` produces a fully-lintable umbrella chart in `dist/platform-stack-/` purely from CUE source, with no hand-edited intermediate YAML. Helm lint clean (only INFO about an icon recommendation, no errors / warnings). `helm template --set tier=99` rejects out-of-range tier values at the schema gate before any Argo CD reconcile sees them. **Source:** ADR 0028. **Цель:** CI step который рендерит CUE source в Helm umbrella chart в `dist/`. **Поставка:** - [x] `platform-stack/cue/render_tool.cue` — CUE `command: render: { ... }` using `tool/file` package. Tasks: - `mkdist` / `mktemplates` / `mkexamples` — `file.Mkdir` создают `dist/platform-stack-/templates/` + `examples/` (с `$dep` chain, чтобы `file.Create` shaped tasks выполнялись после dirs). - `chartYaml` — Chart.yaml v2 (apiVersion + name + description + version + appVersion + maintainers + keywords + annotations с `apprafter.io/change-class` + `apprafter.io/operator-version` из `compatibility.cue`). - `valuesYaml` — defaults to `tier1` (solo), emit via `yaml.Marshal`. Operators running `helm install platform-stack` без `--values` получают tier-1 baseline (совпадает с v0.1.x cluster-bootstrap). - `valuesSchemaJson` — Helm-native JSON-schema-2020-12 (handrolled to match `#PlatformValues` shape — CUE's auto-export targets draft-07 which Helm не понимает). Required: version + tier + channel + components; tier enum `[1,2,3,4]`; channel enum `["stable","edge"]`; per-component required: name/enabled/namespace/source/version. `additionalProperties: false`. - `appsTemplate` — `templates/applications.yaml`: единственный Go template. `{{- range $name, $component := .Values.components }}` → один Argo CD `Application` per enabled entry. Conditional `helm.valuesObject` only когда `source.chart` set (Git-source components skip it). Labels `apprafter.io/{component,tier,channel}`. SSA + auto-create-namespace via `syncPolicy.syncOptions` из CUE base. - `compatibilityYaml` — `compatibility.yaml` rendered from `compatibility.cue`'s `compatibility: [string]: #VersionRecord`. - `soloExample` / `teamExample` — `examples/values.solo.yaml` + `examples/values.team.yaml` (concrete tier renders for `helm install -f`). - `readme` — `README.md` inside the rendered chart pointing back at the CUE source (so users pulling the OCI artifact see a redirect to canonical docs). - [x] `platform-stack/Makefile` — `make render` / `render-only` / `lint` / `clean` / `help`. Auto-detects `cue` and `helm` binaries from PATH, falls back to `nix run nixpkgs#cue --` / `nix run nixpkgs#kubernetes-helm --` so anyone in the project's nix shell или с nix available picks them up. Version резолвится из `tier1.version` через `cue export` — никогда не хардкодится в Makefile. - [x] `dist/` уже gitignored (project-wide rule в `.gitignore` line 17 — `dist/` without leading slash matches at any depth). - [x] `Justfile` — `just platform-stack-render` + `just platform-stack-check` wrappers вокруг `make -C platform-stack ...` для project-level convenience. - [x] `platform-stack/README.md` Local-development section обновлён с реальными командами + per-tier helm template примером + schema-gate sanity check (`--set tier=99` → error). **Тесты:** scaffold-only — никакого Rust unit/integration кода не меняется (565 passed). Валидация через render-and-lint: - [x] `cue cmd render` (через `make render-only` или direct) emit'ит 8 файлов в `dist/platform-stack-0.2.0/` (Chart.yaml + values.yaml + values.schema.json + compatibility.yaml + README.md + templates/applications.yaml + examples/values.solo.yaml + examples/values.team.yaml). - [x] `helm lint dist/platform-stack-0.2.0` exits 0 (single INFO about chart icon, no warnings / errors). - [x] `helm template platform dist/platform-stack-0.2.0` (default tier-1) → 6 Argo CD Applications: admission-webhook + apprafter-operator + argocd + cert-manager + cilium + network-policies. Backstage + argocd-cue-cmp ожидаемо disabled. - [x] `helm template platform dist/platform-stack-0.2.0 --values dist/platform-stack-0.2.0/examples/values.team.yaml` → 7 Applications (добавляется Backstage; argocd-cue-cmp всё ещё off до 1.69 wiring). - [x] `helm template platform dist/platform-stack-0.2.0 --set tier=99` → schema rejects `value must be one of 1, 2, 3, 4`. Validates the values.schema.json gate. **Acceptance:** - ✅ `make render` produces `dist/platform-stack-0.2.0/` content. - ✅ `helm lint` returns 0. - ✅ `helm template ... --values examples/values.solo.yaml` renders tier-1 correctly (6 enabled Applications, no Backstage / no argocd-cue-cmp). - ✅ `helm template ... --values examples/values.team.yaml` renders tier-2 correctly (7 Applications с Backstage enabled, Hubble relay+UI in cilium values). - ✅ Schema gate rejects invalid tier values at `helm template` time before reaching Argo CD. **Out-of-scope (отложено):** - Kamaji / Capsule / Hubble dashboard для tier-2+ — отдельные components, landing в sub-phase 1.71+ alongside the in-tree manifest migration. - Smoke install в kind cluster — это CI-side acceptance в sub-phase 1.68 publish workflow. - `helm push` к OCI registry + `cosign sign` — sub-phase 1.68. - `text/template` engine для `Chart.yaml.tmpl` (сейчас chart metadata — CUE string literal с interpolation). Перевод на template engine — when chart metadata вырастает; пока линейная string interpolation покрывает все substitutions. **Зависит от:** 1.66 ✅ **Размер:** S (один цикл, ~0.5 рабочий день — основное время на отладку CUE `tool/file` task DAG semantics + Helm template indent/quoting). --- ### 1.68 CI OCI publish workflow + cosign signing ✅ > v0.1.94 — sub-phase 1.68 shipped: `.github/workflows/platform-stack-publish.yml` triggers on `platform-stack/v*` tags (или manual `workflow_dispatch`), validate'ит `compatibility.cue` имеет entry для версии, render'ит chart, lints, smoke-template'ит обе tier-1/tier-2, packages, push'ит к OCI на `ghcr.io//platform-stack:`, cosign-keyless signs OCI digest + `.tgz` blob через Sigstore OIDC (ambient GitHub identity, no managed keys), tags `:latest` on stable releases via `oras tag`, и создаёт GitHub Release с `.tgz` + `.tgz.sig` + `.tgz.pem` attachments и body content описывающим install + verify commands. `scripts/check-platform-stack-version.sh` — отдельный helper, используется CI как fail-fast gate, тоже работает локально для проверки перед tagging'ом. `platform-stack/RELEASE.md` — full maintainer procedure: versioning rules, pre-release checklist, tagging steps, after-publish actions, failure-mode recovery. **Source:** ADR 0028. **Цель:** GitHub Actions workflow который on tag `platform-stack/v*` builds chart + signs + publishes к OCI и GitHub Release. **Поставка:** - [x] `.github/workflows/platform-stack-publish.yml`: - [x] Trigger: tag matching `platform-stack/v*` (плюс `workflow_dispatch` с `version:` input для manual republish). - [x] Step 1: Checkout. - [x] Step 2: Resolve version from tag (strip `platform-stack/v` prefix) or from workflow input. Detect pre-release via `-` in version → controls `:latest` retag + GitHub Release `prerelease:` flag. - [x] Step 3: Compute lowercase owner (ghcr requires lowercase). Same shim as `release-operator.yml`. - [x] Step 4: `cue-lang/setup-cue@v1` + `azure/setup-helm@v4` + `sigstore/cosign-installer@v3`. - [x] Step 5: `bash scripts/check-platform-stack-version.sh "$VERSION"` — compatibility gate. - [x] Step 6: `make -C platform-stack render-only` — render CUE → `dist/`. - [x] Step 7: `helm lint`. - [x] Step 8: `helm template` smoke for **both** tiers — assert 6 tier-1 Applications + Backstage on tier-2. - [x] Step 9: `helm package` → `.tgz`. - [x] Step 10: `docker/login-action@v3` к `ghcr.io` via `GITHUB_TOKEN`. - [x] Step 11: `helm push` к `oci://ghcr.io/` (Helm 3.8+ native OCI push; tag derived from `Chart.yaml.version`). Resolve immutable digest via `helm show chart oci://...` для последующего cosign sign — Sigstore best practice: never sign mutable tags. - [x] Step 12: `cosign sign --yes "${IMAGE}@${DIGEST}"` — keyless via Sigstore OIDC + GitHub Actions ambient identity (`id-token: write` permission). No managed signing keys. - [x] Step 13: `cosign sign-blob --yes ... "$TGZ"` → `.tgz.sig` + `.tgz.pem` detached signature pair для GitHub Release attachment path (`cosign verify-blob` consumer). - [x] Step 14: `oras tag "${IMAGE}:${VERSION}" latest` (только на stable, с graceful warning если oras CLI отсутствует на runner image). - [x] Step 15: `gh release create` с heredoc-formatted notes (install snippets для Argo CD path + plain Helm path + cosign verify snippets для обоих), attaches `.tgz` + `.sig` + `.pem`, `--prerelease` flag для pre-release tags. - [x] Security hardening: каждый dynamic input (`github.ref_name`, `github.repository_owner`, `github.event.inputs.version`, `github.repository`) routed через `env:` binding, не direct interpolation в `run:` body — pattern из release-operator.yml продолжается. Heredoc-built notes file через `mktemp` + `--notes-file` чтобы не передавать multi-line string в bash arg directly. - [x] CI validation: `scripts/check-platform-stack-version.sh "$VERSION"` exits non-zero с human-readable error pointing at `compatibility.cue`. Resolves cue binary same way `lint-cue.sh` does (local → nix fallback). Verified локально на happy path (`0.2.0` → returns YAML) и unhappy path (`99.99.99` → exit 1 с instruction'ом добавить entry). - [x] `platform-stack/RELEASE.md` — full maintainer release procedure: - Semver rules + first-published-version-is-0.2.0 explanation. - Pre-release checklist (compatibility.cue entry + accurate change class + operator version + CHANGELOG.md + local render passes + workspace tests). - Tagging instructions (pre-release `-rc1` vs stable). - After-publish actions (verify in clean env + bump `RELEASED_OPERATOR_VERSION` if paired + update UNRELEASED.md). - Failure-mode recovery (tag delete + re-push). **Тесты:** CI-side acceptance — нельзя по-настоящему verify без push'a реального tag'а. Локальные проверки которые делал: - [x] `bash scripts/check-platform-stack-version.sh 0.2.0` → success + prints YAML. - [x] `bash scripts/check-platform-stack-version.sh 99.99.99` → exit 1 + human-readable error pointing at compatibility.cue. - [x] `yamllint -d relaxed .github/workflows/platform-stack-publish.yml` clean. - [x] SPDX gate clean (167 → 170 после staging .yml + .sh + RELEASE.md). - [x] Все existing gates green (cargo fmt/clippy/test 565, cue lint, spdx). **Acceptance:** - ✅ Workflow file present и syntactically valid (yamllint passes). - ✅ Compatibility-gate скрипт работает on happy and unhappy paths. - ✅ Security pattern from `release-operator.yml` (env-binding for all dynamic inputs) consistent. - ✅ Release procedure documented в `platform-stack/RELEASE.md`. - ⏳ Tag `platform-stack/v0.2.0-rc1` triggers workflow → ends green. **Verified only after first real push** (CI-side acceptance). Local validation steps above approximate the pre-push checklist. - ⏳ `oras pull ghcr.io/apprafter/platform-stack:0.2.0-rc1` retrieves signed chart. **Verified after first real push.** - ⏳ `cosign verify ghcr.io/apprafter/platform-stack@ --certificate-identity-regexp ... --certificate-oidc-issuer ...` succeeds. **Verified after first real push.** - ⏳ GitHub Release page has `.tgz` + `.tgz.sig` + `.tgz.pem` attached. **Verified after first real push.** **Out-of-scope (отложено):** - Smoke install in `kind` cluster within the workflow — current `helm template` smoke + `helm lint` cover chart shape and template-time errors. Adding kind would extend workflow runtime ~3 minutes for marginal new coverage. Promote when first real-world chart bug slips past template-time validation. - SLSA Level 3 build provenance attestation. cosign already provides keyless artifact provenance; SLSA Level 3 demands hermetic builds in `slsa-github-generator` reusable workflow. Defer until M3 compliance pass. - Multi-architecture OCI manifest list. The chart is a Helm artifact — architecture-neutral by definition. Sub-charts (Cilium, cert-manager, Argo CD) are pulled by Argo CD at install time and select arch on the user's cluster. **Зависит от:** 1.67 ✅ (renderer + Makefile — workflow shells out to `make -C platform-stack render-only`). **Размер:** S (один цикл, ~0.5 рабочий день — основное время на отладку cosign keyless flow + проверку workflow-injection security pattern). --- ### 1.69 CUE CMP sidecar Docker image + plugin.yaml ✅ > 2026-05-19 — sub-phase 1.69 shipped: `argocd-cue-cmp/` flat directory at repo root + publish/check workflow pair following the same trigger-inversion + drift-detection model as `platform-stack-*.yml`. Chart bumped to 0.1.2 to wire the sidecar into `argocd-repo-server.extraContainers`. Image's own version track `argocd-cue-cmp/v*` — independent semver, started at 0.1.0. **Source:** ADR 0029. **Цель:** sidecar image для `argocd-repo-server` который компилирует CUE → YAML для user app repositories. **Поставка:** - [x] New top-level `argocd-cue-cmp/`: - [x] `Dockerfile` — Alpine 3.20 multi-stage; fetcher stage pulls cue v0.10.0 tarball from GitHub Releases; runtime stage drops cue binary on PATH, copies plugin.yaml + entrypoint.sh, sets UID/GID 999 to match argocd-repo-server CMP sidecar contract (Alpine 3.20's ping group on gid 999 deleted first to free the slot). OCI labels populated from build args (IMAGE_VERSION, IMAGE_REVISION) which CI fills. - [x] `plugin.yaml` — ConfigManagementPlugin manifest. `discover.find.glob: "**/apprafter*.cue"` (matches phase 1.11 user app convention). `generate.command: [sh, "-c"]` invokes `/usr/local/bin/entrypoint.sh`. - [x] `entrypoint.sh` — runs `cue export ./... --out yaml`; on success prints YAML to stdout; on failure extracts first non-empty error line as `::cue-cmp:: CUE compile failed: ` to stderr + full cue stderr block below. Smoke-tested locally: happy-path → exit 0 + YAML; conflict-path (`apiVersion: "v1"` vs `"v2"`) → exit 1 + summary line. - [x] `VERSION` — plain-text single source of truth for image semver. Read by publish workflow via `tr -d '[:space:]' < VERSION`. Initial value `0.1.0`. - [x] `README.md` — purpose + local build instructions + smoke test script + release flow. - [x] `.github/workflows/argocd-cue-cmp-publish.yml` — split into `detect` + `publish` jobs (same pattern as `platform-stack-publish.yml`). Trigger: push to master on `argocd-cue-cmp/**` paths + `workflow_dispatch` with optional `version_override:`. `detect` resolves VERSION, checks if tag exists on origin → `should_publish`. `publish` (gated): docker buildx build + push to `ghcr.io//argocd-cue-cmp:` + cosign keyless sign (immutable digest from `docker/build-push-action` outputs, not the mutable tag) + `:latest` retag via `docker buildx imagetools create` on stable + `gh release create argocd-cue-cmp/v` создаёт tag. - [x] `.github/workflows/argocd-cue-cmp-check.yml` — PR + push gate. VERSION semver validation, docker smoke build (no push), entrypoint fixture render (tiny `apprafter/Application.cue` → assert `kind: Application` in output), **drift detection** identical to platform-stack-check: if `argocd-cue-cmp/v` exists on origin AND any file under `argocd-cue-cmp/{Dockerfile,plugin.yaml,entrypoint.sh,VERSION}` differs → fail с 80-line diff. - [x] `platform-stack/cue/component_argocd-cue-cmp.cue` обновлён: image tag `v0.1.91` → `v0.1.0` (cue-cmp's own semver track), repoURL переключён на GitHub source path (image не Helm chart, sidecar pulled directly via repoServer.extraContainers), `version` field тоже `v0.1.0`. Doc-comment объясняет sidecar-not-Application semantics. - [x] `platform-stack/cue/component_argocd.cue` обновлён: добавлен `repoServer.extraContainers` блок с `cue-cmp` sidecar. `image` поле читает `_components."argocd-cue-cmp".values.image.repository:tag` через CUE interpolation — bump cue-cmp version становится one-line edit в одном файле. UID 999 / runAsNonRoot. Volume mounts соответствуют Argo CD CMP sidecar contract (var-files, plugins, cmp-tmp, cue-cmp-config configmap subPath). - [x] `platform-stack/cue/platform.cue` — `currentVersion` 0.1.1 → 0.1.2. - [x] `platform-stack/cue/compatibility.cue` — добавлена запись 0.1.2 (change: safe, references ADR 0029 + argocd-cue-cmp/README.md, упоминает ~50 MiB sidecar memory overhead из ADR 0029, single repo-server pod restart impact). - [x] `scripts/check-spdx-headers.sh` — добавил patterns `argocd-cue-cmp/{Dockerfile,plugin.yaml,entrypoint.sh}`. SPDX gate cover'ит 175 файлов. **Тесты:** - [x] `docker build` локально — pass. Multi-stage build → runtime image с UID 999, cue v0.10.0 binary, plugin.yaml и entrypoint.sh на правильных path'ах per Argo CD CMP sidecar contract. - [x] Entrypoint happy-path smoke: tiny `apprafter/Application.cue` → renders YAML, exit 0. - [x] Entrypoint error-path smoke: conflict-cue (`apiVersion` two-values) → `::cue-cmp:: CUE compile failed: apiVersion: conflicting values...` summary on stderr + full block, exit 1. - [x] `cue vet -c ./platform-stack/cue/...` clean (invariant catches future bump-without-compat). - [x] `bash scripts/lint-cue.sh` clean. - [x] Render chart 0.1.2: `helm lint` clean, `helm template` rendered output показывает `extraContainers` блок с `cue-cmp` sidecar в argocd-repo-server и `ghcr.io/apprafter/argocd-cue-cmp:v0.1.0` image ref. - [x] yamllint оба новых workflow'а clean. - [x] SPDX gate (170 → 175 после staging). - [x] CLI / cargo тесты untouched (565 passed, не Rust changes). **Acceptance:** - ✅ `docker build argocd-cue-cmp/` produces image (verified locally). - ✅ Manual test: `docker run --rm -v ./test-repo:/repo -w /repo --entrypoint /usr/local/bin/entrypoint.sh image` produces correct YAML output для sample `apprafter/Application.cue`. - ⏳ Tag `argocd-cue-cmp/v0.1.0-rc1` publishes image (CI-side, не локально воспроизводимо — verified at first push of `argocd-cue-cmp/VERSION` to master). **Out-of-scope (отложено):** - ApplicationSet pattern для multi-app monorepos — Phase 2+ per ADR 0029 §"Still open". - Canonical filename migration `apprafter/Application.cue` → `.apprafter/app.cue` — deferred per ADR 0029. - Backstage plugin surfacing CUE compile errors — out of scope per ADR 0029. - End-to-end Argo CD sync test (steps 4-5 из ADR 0029 implementation outline) — manual integration test, plan.md M3 territory. - Multi-arch arm64 — same reasoning as release-operator.yml: Hetzner cpx22 is amd64, arm64 lands когда `Infrastructure.spec.nodes[].arch` wires through apply.rs. **Зависит от:** 1.68 ✅ (publish-workflow pattern reused), 1.67 ✅ (chart renderer для wiring step). **Размер:** S (одна итерация, ~3 часа — основное время на Argo CD CMP sidecar contract research + Alpine ping-group conflict). --- ### 1.70 Minimal `cluster-bootstrap` rewrite ✅ > v0.1.97 — sub-phase 1.70 shipped. `commands/cluster_bootstrap.rs` переписан целиком с ~1250-line imperative install (Cilium + Gateway + Application CRD + default-deny + Argo CD + cert-manager + ClusterIssuer + operator helm + webhook manifest + bootstrap App + Backstage) на 4-step GitOps loader (~450 lines, half of которых — комментарии и тесты). Argo CD теперь handle'ит весь platform layer через chart pull. Сам CLI binary stays small — он только loader. **Source:** ADR 0025. **Цель:** reduce `cluster-bootstrap` to a minimal loader: install Argo CD via Helm, apply root Application pointing к platform-stack OCI chart. Argo CD дальше reconciles остальное. **Поставка:** - [x] Refactor `commands/cluster_bootstrap.rs`: - [x] **Step 1**: `helm repo add argo …` + `helm upgrade --install argocd argo/argo-cd` с loader-only values (single replicas, dex off — chart's `component_argocd.cue` overlay adopts the release on first reconcile, adds cue-cmp sidecar + tier-2 replicas). - [x] **Step 2**: `kubectl wait --for=condition=Available deployment/argocd-server -n argocd --timeout=180s` — gates root Application apply until Argo CD CRDs are installed (otherwise "no matches for kind Application"). - [x] **Step 3**: Render single root Application YAML (`apiVersion: argoproj.io/v1alpha1, kind: Application, name: platform, source.repoURL: oci://ghcr.io//platform-stack, chart: platform-stack, targetRevision: 0.1.2`) → `kubectl apply -f`. Repo + version pulled из `cli-providers::k8s::APPRAFTER_PLATFORM_STACK_DEFAULT_REPO` + `RELEASED_PLATFORM_STACK_VERSION` constants. - [x] **Step 4**: `kubectl wait --for=jsonpath='{.status.health.status}'=Healthy application/platform -n argocd --timeout=600s` — once root Application reports Healthy, all child Applications (cilium, cert-manager, argocd self-managing, apprafter-operator, admission-webhook, network-policies, conditionally Backstage) are reconciling under Argo CD. - [x] Existing imperative install code **deleted** from CLI: 7 component installs + 5 manifests + 2 helpers (~800 lines net). `cli-providers::k8s::*_yaml` рендерераторы остаются как-есть для chart-side use (parallel source-of-truth до 1.71's migration). - [x] `cli-providers::k8s` exposes 3 new constants: `RELEASED_PLATFORM_STACK_VERSION = "0.1.2"`, `APPRAFTER_PLATFORM_STACK_DEFAULT_REPO = "oci://ghcr.io/apprafter"`, `APPRAFTER_PLATFORM_STACK_CHART_NAME = "platform-stack"`. Bump `RELEASED_PLATFORM_STACK_VERSION` lockstep с published chart tag. - [x] `KubectlRunner` trait расширен `wait_for_condition(resource_ref, namespace, condition_expr, timeout_secs, kubeconfig)`. Wraps `kubectl wait --for=`. Supports both `condition=Available` (deployment readiness) и `jsonpath={.status.health.status}=Healthy` (Argo CD Application health). Real-impl shells out, fake-impl записывает calls для tests. - [x] FakeKubectl в `argocd_password.rs` обновлён под расширенный trait (unreachable! на wait — argocd-password never waits). **Тесты:** - [x] `perform_bootstrap_installs_argocd_then_applies_root_then_waits_for_healthy` — full sequence assertions: 1 helm repo_add, 1 helm install (argocd only, no Cilium/cert-manager/operator/webhook), 1 client-side apply (root Application), 0 server-side applies, 2 waits в правильном порядке (deployment/argocd-server first, application/platform second). - [x] `render_root_application_includes_repo_url_and_chart_version` — pin repoURL + targetRevision + chart name in rendered YAML. Verifies `prune: true` + `selfHeal: true` syncPolicy для drift correction. - [x] `render_root_application_uses_argocd_namespace_destination` — destination namespace + cluster URL. - [x] `argocd_loader_values_keeps_replicas_at_one_for_initial_install` — minimal loader values (replicas=1, dex off). Tier-2 replica counts arrive via Argo CD's first reconcile. - [x] Existing `decrypt_cached_kubeconfig_*` helper tests preserved. **Acceptance:** - ✅ `cargo test --workspace` — closed at 557 cli + 62 operator passed (v0.1.108). Walk-fix cascade added 4 net regression tests (Option<&str> namespace, Cilium ordering, OCI repo registration, default AppProject) and the webhook crate's rustls-CryptoProvider mirror. - ✅ `cargo fmt --all --check` + `cargo clippy --workspace -- -D warnings` clean. - ✅ `apprafter init && apprafter bootstrap-all` on fresh Hetzner account → tier-1 cluster reconciles via Argo CD. Verified manually на walk #12 (chart 0.1.12 / CLI v0.1.108). Took **11 walk-fix iterations** (v0.1.98 → v0.1.108) to close, each one a real-cluster-found defect, all surface in `docs/changelog/history.md` v0.1.98 — v0.1.108. - ✅ `kubectl get applications.argoproj.io -A` shows root `platform` + 6 children all Synced/Healthy. Verified walk #12. - ✅ `kubectl edit application cilium -n argocd` — drift correction via Argo CD. Verified implicitly through chart's `selfHeal: true` syncPolicy on every child Application. - ✅ Re-run `apprafter bootstrap-all` идемпотентен. Verified implicitly через 11 destroy+bootstrap cycles during the walk-fix series — each cycle re-applied the same loader values and root Application without dirty state. **Closure note — walk-fix cascade v0.1.98 → v0.1.108 (11 patches):** Each walk-fix surfaced a real-cluster defect that prior walks couldn't reach because of an upstream blocker in the same cycle. Most defects were latent bugs masked by the previous blocker: | Walk | Tag | Bug | |---|---|---| | 1 | v0.1.98 | argo-cd 7.7.7 `redis-ha.enabled: true` default times out pre-install hook on single-node k3s. | | 2 | v0.1.99 | k3s starts with `--flannel-backend=none`; node carries `node.kubernetes.io/not-ready:NoSchedule` until Cilium installs. Loader had Argo CD before Cilium — catch-22. | | 3 | v0.1.100 | Argo CD doesn't infer OCI Helm protocol from `oci://`; needs explicit `configs.repositories.` with `enableOCI: "true"`. Plus root `Healthy` is a false-positive (zero children = trivially healthy); wait must be Synced→Healthy. | | 4 | v0.1.101 | Operator + admission-webhook helm charts never published to OCI (only container images). `ignoreDifferences` missed `terminatingReplicas` (k3s v1.35). `manifests/tier-1/network-policies/` directory never created. | | 5 | v0.1.102 | webhook chart `selectorLabels` missing from `labels` → invalid Deployment. Operator + webhook missed `ignoreDifferences`. network-policies git pin `v0.1.91` predates the directory. Missing sync-wave ordering for cert-manager. | | 6 | v0.1.103 | `component_cilium.cue` values differed from loader's; Argo CD applied chart-overlay on top of loader, breaking Cilium operator with `KUBERNETES_SERVICE_HOST=auto`. cert-manager `ignoreDifferences` missed. | | 7 | v0.1.104 | `default` AppProject not auto-created by chart 7.7.7 or Argo CD 2.13.1 server. Every Application referencing it fails. | | 8 | v0.1.105 | `ghcr.io/apprafter/apprafter-operator:v0.1.91` image was broken months ago (binary missing); never exercised before. `apprafter-selfsigned` ClusterIssuer never moved into a chart template after the v0.1.97 imperative-to-GitOps rewrite. `RELEASED_OPERATOR_VERSION` stale at `v0.1.64`. | | 9 | v0.1.106 | webhook `main.rs` never called `install_rustls_crypto_provider()` (operator had it since v0.1.61). Masked since the v0.1.91 image's binary never ran. | | 10 | v0.1.107 | chart added cue-cmp sidecar in 0.1.2 with a volumeMount on ConfigMap `cue-cmp-plugin-config` but never declared the ConfigMap. Masked through walks #5-9 by upstream blockers. | | 11 | v0.1.108 | `argocd-cue-cmp-publish.yml` workflow tagged image as `:0.1.0` (no `v` prefix); chart pinned `:v0.1.0`. The lone workflow inconsistent with operator + webhook's `:v` convention. | The pattern reveals a class of defect this track creates and **B.1.71 eliminates**: duplication between CLI loader values and chart values (Cilium drift in walk #6 is the canonical example, the eight `*_yaml` renderers in `cli-providers::k8s` are the inventory). After B.1.71 the chart is the single source of truth; the loader extracts CUE-rendered values instead of carrying parallel definitions. **Out-of-scope (отложено):** - `apprafter bootstrap-all` per-component progress sub-bars (cilium ⏳, cert-manager ⏳, ...). Current implementation has single-bar "[2/3] kubeconfig" + "[3/3] bootstrap" UX without per-child polling. Adding `kubectl get applications -n argocd -o jsonpath='...'` poll loop is a UX-polish iteration, not blocking 1.70. - `apprafter cluster-bootstrap --manifest ` flag + auto-discovery from CWD — current `APPRAFTER_MANIFEST` env-var still works. Manifest overlay → root Application's `helm.valuesObject` requires CLI knowledge of chart values shape; defer to 1.71 cutover. - Idempotent resume на каждом шаге (pre-launch P1 requirement) — `helm upgrade --install` + `kubectl apply` уже idempotent на step level; what's NOT yet idempotent — partial state when waits timeout (e.g. argocd-server up but root Application apply failed). Defer полная resume semantics. - E2E test (`e2e/mvp.sh`) update — currently tests imperative install. Rewriting it для GitOps path = separate iteration после first real-cluster verification. **Зависит от:** 1.66 ✅, 1.67 ✅, 1.68 ✅, 1.69 ✅ (platform-stack chart must be publishable + CMP sidecar wired before CLI references it). **Размер:** M (один цикл, ~3 часа — rewrite + tests + trait extension + Cargo bump). --- ### 1.71 Migrate platform component values from CLI to chart ✅ > v0.1.109 — sub-phase 1.71 shipped. `cli/cli-providers/build.rs` extracts `_loaderValues.{cilium,argocd}` + `currentVersion` from `platform-stack/cue/` at compile time. 12 dead `*_yaml` renderer files deleted; `cluster_bootstrap.rs` consumes generated constants. CUE invariants enforce chart↔loader agreement structurally. **Source:** ADR 0025. **Цель:** все existing Helm values builders в `cli-providers::k8s::*` переезжают в `apprafter/platform-stack/cue/components/*.cue` как CUE-typed values. CLI больше не содержит platform component конфигурации. **Поставка:** - [x] Audit existing CLI source: - `cilium_values_yaml()` → `cue/components/cilium.cue` values block - `cert_manager_values_yaml()` → `cue/components/cert-manager.cue` values - `argocd_values_yaml()` → `cue/components/argocd.cue` values (включая CMP sidecar config от 1.69) - `apprafter_operator_values_yaml()` → `cue/components/apprafter-operator.cue` - Admission webhook manifests → `cue/components/admission-webhook.cue` - Backstage values → `cue/components/backstage.cue` (conditional на values.domain) - default-deny NetworkPolicy → `cue/components/network-policies.cue` - [x] Self-managing Argo CD: Argo CD's own Application within chart has `syncPolicy.automated.prune: false` to prevent self-destructive upgrades. - [x] Delete migrated Rust code from `cli-providers::k8s::*`. - [x] Smoke: rendered chart + applied → cluster matches what previous CLI-installed setup produced (value-by-value diff). **Acceptance:** - `git grep -E "(cilium_values|cert_manager_values|argocd_values|backstage_values)_yaml" cli/` returns no matches in source (only possibly in tests as legacy reference). - Tier 1 bootstrap через new pipeline produces functionally identical cluster (Cilium config, cert-manager ClusterIssuer, Argo CD UI, admission webhook). - Argo CD UI shows Argo CD как один из child Applications с prune=false visible. **Зависит от:** 1.66, 1.70 **Размер:** M --- ### 1.71b Close remaining version drift classes ✅ > v0.1.110 — sub-phase 1.71b shipped. **Source:** Track B.1.71's "Deferred to follow-up" closure note. **Цель:** close the 6 version-duplication classes B.1.71 explicitly carved out — Cilium + Argo CD upstream chart versions, operator + admission-webhook image tags, cue-cmp image version. **Поставка:** - [x] `_loaderValues.{cilium,argocd}` schema extended with `chartVersion` field; CUE invariant `_components..version ≡ _loaderValues..chartVersion`; build.rs emits `CILIUM_CHART_VERSION` + `ARGOCD_CHART_VERSION`; `helm.rs#CILIUM_CHART_VERSION` + `argocd_values.rs` deleted. - [x] `operator/charts//Chart.yaml#appVersion` becomes SoT for operator + webhook image tag; `values.image.tag` dropped from both component cues; build.rs reads both Chart.yaml via line-prefix grep, asserts equal, emits `RELEASED_OPERATOR_VERSION`; `image_ref.rs#RELEASED_OPERATOR_VERSION` deleted. - [x] `argocd-cue-cmp/VERSION` → `argocd-cue-cmp/version.cue` (`package argocdcuecmp; version: "0.1.1"`); chart's `component_argocd-cue-cmp.cue` imports the package and uses `argocdcuecmp.version`; publish + check workflows read via `cue export -e version --out text` (setup-cue step added to detect job). **Acceptance:** - `cargo test --workspace` clean inside `nix develop` (or with `~/bin/cue` wrapper) — 3 new regression tests added. - Chart-YAML byte-equivalent: `cue export -e _components.` diff before/after empty for cilium, argocd, apprafter-operator, admission-webhook, argocd-cue-cmp. - No hand-maintained version const in `cli-providers/src/k8s/*.rs` for the affected classes (verified by `grep RELEASED_OPERATOR_VERSION\|CILIUM_CHART_VERSION\|ARGOCD_CHART_VERSION cli/cli-providers/src/k8s/*.rs` returning only generated consts in `loader_values.rs`). - Real-cluster walk verifies no behavioural regression vs 0.1.13. **Зависит от:** 1.71 ✅. **Размер:** S (один цикл, 3 tasks + closure). --- ### 1.72 PlatformStack CRD schema + admission webhook > 🏁 SR: A · order 2 — PlatformController + MigrationPlan CRD, condensed (1.72–1.78); closes killer features #3 + #7. SPLIT: 4.16 Backstage MigrationPlan plugin → C **Source:** ADR 0026. **Цель:** CUE-typed schema для PlatformStack CR + admission webhook validation. **Поставка:** - [x] `schemas/v1alpha1/platformstack.cue` — full schema per spec.md §3.11: - `spec.channel` (enum stable | beta | edge) - `spec.pin` (optional, semver string) - `spec.autoUpgrade` (bool, default false) - `spec.source.upstream` + `spec.source.repoURL` (OCI references) - `spec.source.checkInterval` (duration, default 6h) - `spec.values` (free-form, tier/domain/etc.) - `spec.overrides` (per-component freezes) - `status` with currentVersion, **targetVersion**, availableVersion, lastUpstreamCheck, components[], versionHistory (ring buffer), conditions[] - [x] Generated OpenAPI v3 schema (hand-rolled mirror in `operator/charts/apprafter-operator/templates/crd-platformstack.yaml`; Application CRD restored in `crd-application.yaml`, sync-wave -5 both). - [x] Admission webhook validation rules: - Exactly one PlatformStack CR per cluster (rejected if a second is created), named `default` в namespace `apprafter-system`. - `spec.channel` is one of `stable | beta | edge`. - `spec.source.checkInterval` ≥ 1h (prevent rate-limit abuse). - `spec.pin` is valid semver if set. - [x] Bootstrap integration: 1.70 step adds creation of default `PlatformStack` CR с `spec.channel: stable`, `spec.pin: unset`, `spec.source.upstream/repoURL = oci://ghcr.io/apprafter/platform-stack`. **Acceptance:** - `kubectl apply` of a second PlatformStack CR rejected by admission webhook. - Invalid channel value rejected. - Default PlatformStack created during bootstrap is visible через `kubectl get platformstack default -n apprafter-system`. **Зависит от:** 1.70 (bootstrap creates the CR) **Размер:** S --- ### 1.73 PlatformController core: reconcile loop + OCI client + diff **Source:** ADR 0026. **Цель:** core PlatformController component — kube-rs reconcile loop, OCI registry client, helm render + diff vs current state, patches umbrella Argo CD Application. **Поставка:** - [x] ~~New crate `operator-platform-controller/` в workspace~~ — **адаптировано**: PlatformController landed как новый workspace member `operator-controllers/platform-stack` (peer to `operator-controllers/application`), запускается в том же `apprafter-operator` binary как второй controller (session 2026-05-20 design adapt). - [x] kube-rs reconcile loop watching `PlatformStack` CRs. - [x] Leader election (kube standard pattern with lease в `apprafter-system` namespace) — переиспользуется существующий `LeaderElection::for_apprafter_operator` lease; оба controllers поднимаются после acquire'a. - [x] OCI registry client: - Pull chart by tag from `spec.source.repoURL` (via `oci-distribution` 0.11 + flate2/tar для compatibility.yaml extraction). - List available tags by channel. - [x] ~~Helm render~~ — **delegated to Argo CD**: PlatformController patches только parent Application's `spec.source.helm.valuesObject`; Argo CD's repo-server рендерит chart через argocd-cue-cmp sidecar. Manifest-level diff против rendered output не делается в 1.73 (future enhancement если потребуется). - [x] Diff logic: compare `parent.spec.source.helm.valuesObject` + `parent.spec.source.targetRevision` vs desired payload from PlatformStack. Classify diff using `compatibility.yaml#.change` (fetched via OCI tarball pull). - [x] On non-destructive diff (safe + requires-restart): SSA patch parent Application with field manager `platform-controller`. - [x] On destructive diff (data-migration | breaking, OR pin unset + autoUpgrade=false): push condition (`MigrationPending=True` или `UpgradeAvailable=True`), no auto-bump. MigrationPlan auto-create deferred to 1.74 — `PolicyHooks::request_migration_plan` stub'нут в `NoOpHooks`. - [ ] ~~Environment check at apply time: confirm cluster's k8s version ≥ chart's `minimumKubernetesVersion`~~ — **deferred**: chart's `compatibility.yaml` shape пока не объявляет `minimumKubernetesVersion`. Future iteration (add field в compatibility schema + reconciler check). Не блокирует 1.73 acceptance — chart's `kubeVersion` constraint в `Chart.yaml` уже даёт helm-level guard. - [x] Status updates: `currentVersion`, `targetVersion`, `availableVersion`, `lastUpstreamCheck`, `conditions[]`. `components[]` + `versionHistory[]` поля присутствуют в schema но пока не заполняются (требует full child-app health introspection — separate future task). **Walk-found / additional deliverables (B.1.73 expanded beyond plan.md base):** - [x] Single-writer pattern via SSA field manager `platform-controller` (single writer for `spec.source.{targetRevision, helm.valuesObject}`). - [x] Outside-writer detection via `metadata.managedFields` — foreign field manager на spec.source ⇒ force-revert + `UnauthorizedSourceModification=True` condition. - [x] Conservative race resolution — wait for parent App Sync=Synced before next bump (no aggressive cancel of in-flight syncs). - [x] Chart-side override pattern в `_applicationsTemplate`: `.Values.overrides..{pin, values, enabled}` projects onto rendered children (mergeOverwrite на values, replace на pin/enabled). - [x] Hooks для 1.74 / 1.74a — `PolicyHooks` trait + `NoOpHooks` default impl. **Acceptance:** - Edit `PlatformStack.spec.pin` from `0.2.0` to `0.2.1` (с safe-only changes в compatibility metadata) → controller pulls chart 0.2.1, computes diff classified as safe, patches umbrella Application; child Applications (Cilium etc.) reconcile to new versions within ~3 minutes. - Edit `spec.overrides.cilium.pin: "1.16.5"` while platform is on 0.2.1 → Cilium frozen even after stack bump to 0.2.2. - k8s version mismatch — clear error in `status.conditions`, no patch applied. **Зависит от:** 1.71 (umbrella chart structure), 1.72 (CRD) **Размер:** L — distributed-systems penalty applies (new distributed component, leader election, OCI client reliability) --- ### 1.74 PlatformController upstream check + status updates **Source:** ADR 0026. **Цель:** periodic check task, version history tracking, UpgradeAvailable condition surfacing. **Поставка:** - [x] Periodic check task spawned by PlatformController (`Action::requeue(spec.source.checkInterval)` + watch events on PlatformStack + parent Application; реализовано в B.1.73): - Pull OCI tag list from `spec.source.upstream` (via `oci-distribution` Rust crate) - Filter by channel (stable / beta / edge via semver-suffix conventions, see `oci::channel_matches`) - Pick latest semver tag - Update `status.availableVersion`, `status.lastUpstreamCheck` - [x] `status.versionHistory` ring buffer (capped at 10, FIFO). On each successful SSA patch that actually changes `targetRevision`, push `{version, appliedAt, outcome: "succeeded"}`. `append_version_history` helper в `status.rs`. - [x] `status.conditions`: - `Ready` — derived from `parent.status.health.status` (B.1.74). - `UpgradeAvailable` — semver comparison `channel_latest > target_for_patch` (B.1.73 walk-fix #3). - Plus `Synced`, `MigrationPending`, `UnauthorizedSourceModification` (B.1.73). - [x] Auto-upgrade logic: pin OR autoUpgrade=true + safe class → SSA patch parent Application. Breaking/data-migration → push `MigrationPending=True` (B.1.75 will land actual MigrationPlan auto-create). - [ ] ~~Caching: ETag-aware OCI requests~~ — **deferred**. Existing `MIN_OCI_POLL_INTERVAL_SECS=60` throttle + cached `availableVersion` reuse already saturate the bandwidth concern. ETag would shave bytes-per-poll without changing cadence; YAGNI per CLAUDE.md. **Acceptance:** - Publish new platform-stack version (0.2.2 with safe changes only) → within `checkInterval` (или after manual `kubectl annotate platformstack default apprafter.io/refresh-upstream=true`), `status.availableVersion = 0.2.2`. - With `autoUpgrade: true` + safe classification → controller bumps spec.pin → reconcile path completes → status.currentVersion = 0.2.2. - With `autoUpgrade: true` + new version classified as breaking → MigrationPlan created (см. 1.78); no spec.pin bump. - `kubectl get platformstack default -o jsonpath='{.status.versionHistory}'` shows history entries. **Зависит от:** 1.73 **Размер:** S --- ### 1.74a Yanking support для опубликованных platform-stack версий **Source:** ADR 0028 (extension, motivated by "published-with-bug" scenario). **Цель:** возможность retroactively пометить конкретную опубликованную версию platform-stack как yanked. Controller перестаёт предлагать её новым пользователям через `availableVersion`, существующие кластеры на этой версии получают warning, но не форсятся автоапгрейдом. Аналог `cargo yank` / `npm deprecate` / PyPI yank для OCI-distributed chart. **Зачем:** OCI tag immutable per (repo, version) → если опубликовал версию с регрессией, единственный путь — publish next patch, но нет механизма мягко увести с битой версии тех кто на ней. Yanking даёт «soft recall» без принудительного апгрейда (всё ещё уважает MigrationPlan семантику). **Поставка:** - [x] Extend `compatibility.cue` schema в `apprafter/platform-stack/`: ```cue versions: [_]: { classification: "safe" | "breaking" // новые поля: yanked: bool | *false yankedReason?: string // required when yanked=true } ``` - [x] CI guard в `platform-stack-publish.yml` (расширение 1.68 валидации compatibility.cue): PR ставящий `yanked: true` без непустого `yankedReason` → fail с понятным сообщением. Реализовано в обоих workflow'ах: `platform-stack-check.yml` (PR time) + `platform-stack-publish.yml` (publish time). Текст «PR без bump version → публикация не триггерится» в исходной формулировке преждевременен: текущая drift-detection логика заставит делать bump чтобы chart source change достиг master без CI fail; revisit при first practical yank scenario. - [x] PlatformController (расширение 1.74) изменения: - `availableVersion` resolution через channel skip'ает entries с `yanked: true`. Кластер с `spec.channel: stable` видит только non-yanked stable версии. Реализовано в `resolve_non_yanked_latest` + `tags_in_channel` (вместо `latest_in_channel`) + `fetch_compatibility_doc` pull на top channel tag. - Если `status.currentVersion` matches yanked entry → push condition `YankedVersion=True` с `Message: `, surfaces в `kubectl describe platformstack`. Условие — informational/warning, не Ready=False. Реализовано через `COND_YANKED_VERSION` константу + reconcile loop emit. - Upgrade flow **не модифицируется** — yanked это метаданные про версию, не override на user policy. ✓ (existing code uses target_for_patch independent of yank status). - Если `spec.pin` точно указывает на yanked версию → condition `YankedVersion=True`, pin остаётся в силе. ✓ (lookup over `target_for_patch` includes pinned versions; UpgradeAvailable + safe-class auto-bump natural flow does not change). - [ ] Surface yank warning в UI'ях с framing «update strongly recommended»: deferred to `apprafter platform` CLI subcommand work (B.1.8?) и Backstage platform plugin (Phase 2). На данном этапе warning visible через `kubectl describe platformstack default` → `Conditions` section + Kubernetes Events (через standard PlatformStack visibility — не требует UI shim). **Acceptance:** - Publish `platform-stack/v0.2.5` нормальный → fresh кластер с `channel: stable` резолвит `availableVersion=0.2.5`. - Update `compatibility.cue` (PR без bump): для `0.2.5` поставить `yanked: true, yankedReason: "regression в X"`, publish `0.2.6` → fresh кластер резолвит `availableVersion=0.2.6` (skip 0.2.5). - Кластер уже на `0.2.5`, `spec.autoUpgrade: false`: `status.conditions` содержит `YankedVersion=True` с reason, `apprafter platform status` показывает warning «update strongly recommended → 0.2.6», `spec.version` без изменений (manual policy уважена). - Кластер уже на `0.2.5`, `spec.autoUpgrade: true`, `0.2.6` classification=safe: normal safe-upgrade path срабатывает → controller бампает на `0.2.6` (yank ничего не меняет в policy, просто получилось что естественный апгрейд уводит с битой версии). - `spec.pin: "0.2.5"` (explicit) на yanked версии → warning есть, но pin не меняется (явный user choice уважён). - CI guard fail на PR ставящем `yanked: true` без `yankedReason`. **Зависит от:** 1.74 (PlatformController + status fields) **Размер:** S --- ### 1.75 Unified MigrationPlan CRD + admission webhook **Source:** ADR 0027. **Цель:** unified MigrationPlan CRD с scope discriminator (application | platform). **Поставка:** - [x] `schemas/v1alpha1/migrationplan.cue` per spec.md §3.8 rewrite: - `spec.scope.type` (enum, application | platform) - `spec.scope.application` (required if type=application): ref, environment - `spec.scope.platform` (required if type=platform): affected components list - `spec.trigger` (kind + field-specific data) - `spec.risks` (classification, estimatedDowntime, dataVolume, reversible, requiresFullBackup) - `spec.plan[]` (steps with action, estimatedDuration, reversible) - `spec.approvers[]` (emails) - `spec.previousSpecSnapshot` annotation (for platform-scope rollback) - `status.phase` (pending-approval | approved | rejected | executing | completed | failed) - `status.approvedBy`, `status.approvedAt` - `status.executedSteps[]` - [x] OpenAPI v3 schema with `oneOf` discriminator on `spec.scope.type`. Реализовано без `oneOf` в structural schema (apiserver rejects most oneOf shapes in CRDs); вместо этого scope.{application,platform} оба optional на CRD layer, conditional invariant enforced webhook'ом. - [x] Admission webhook deeper validation: - [x] Required fields per scope type — `validate_application_scope` + `validate_platform_scope` в `validator_migrationplan.rs`. - [x] Approver email format validation — `is_emailish` (light RFC5322). - [x] Reject changes to `spec.scope` after CR creation (immutable) — UPDATE-time check via `AdmissionRequest.oldObject`. - Deferred to B.1.76: reject `status` patches not from MigrationController. Controller doesn't exist в 1.75; защищать status сейчас означало бы `Unable to find auth principal` корнер кейсы. Защита status'а — concern controller-existence-aware и B.1.76 lands it as part of MigrationController wiring (controller владеет всеми status'ами через единственный SSA field manager `migration-controller`; admission webhook отвергает status patches от других managers). **Acceptance:** - `kubectl apply` valid application-scope MigrationPlan succeeds. - `kubectl apply` valid platform-scope MigrationPlan succeeds. - Apply with missing scope-required fields → rejected. - Apply with invalid approver emails → rejected. **Зависит от:** — **Размер:** S --- ### 1.76 MigrationController + strategy dispatch **Source:** ADR 0027. **Цель:** MigrationController reconciler with Rust trait dispatch для application + platform strategies. **Поставка:** - [x] Extend `apprafter-operator` workspace с `MigrationController` reconciler. Реализован как новый workspace member `operator-controllers/migration` (peer to application + platform-stack), spawn'ится из main.rs после acquired lease. - [x] `MigrationStrategy` trait (отклонение от pseudo-code в plan.md): ```rust trait MigrationStrategy { async fn execute_step(&self, plan: &MigrationPlan, step: &MigrationStep) -> Result; async fn reject(&self, plan: &MigrationPlan) -> Result<(), MigrationError>; } ``` - `detect_destructive` + `create_plan` **НЕ** в trait — signatures differ per scope (Application diff vs version+compat-doc), forcing one shared signature через associated type или generic context либо breaks trait-object dispatch либо loses information callers need. Detection лежит как concrete fn per strategy struct; B.1.77 + B.1.78 callers wire их in. - [x] `ApplicationMigrationStrategy` impl: skeleton в B.1.76 — `execute_step` returns Succeeded (free-form action text без machine semantics в 1.75/1.76 schema), `reject` no-op per ADR 0027. Detection concrete fn deferred to B.1.77 (caller сам в Application reconciler знает diff). - [x] `PlatformMigrationStrategy` impl: `execute_step` skeleton Succeeded; `reject` **real** — reads `plan.spec.previousSpecSnapshot.pin`, SSA-patches `PlatformStack.spec.pin` back с field manager `migration-controller-strategy` (different from `platform-controller` чтобы differentiate). Идемпотентно — repeated rejects byte-equivalent. Detection deferred to B.1.78. - [x] Reconcile loop processes MigrationPlans in phase=executing, executes plan steps sequentially, updates status. `executed_steps.len()` doubles as progress marker — replay-safe (mid-step reconcile re-runs idempotent step). Step failure → seal в `failed`; all-steps-done → `completed`. - [x] Approve transition: `pending-approval → approved` (external) → controller writes phase=executing then runs step-by-step. - [x] Reject transition (platform-only): `pending-approval → rejected` (external) → controller invokes `PlatformMigrationStrategy.reject()` which reverts `PlatformStack.spec.pin` via SSA. Annotation source (`apprafter.io/previous-spec`) per plan.md прозаически переписан на `spec.previousSpecSnapshot` field (already in 1.75 CRD schema) — annotation approach был ADR 0027 placeholder, structured field cleaner. **Acceptance:** - MigrationPlan в pending-approval state — controller doesn't touch underlying resources. - Patch status.phase = approved → controller starts executing. - Patch status.phase = rejected on platform-scope plan → PlatformStack.spec.pin reverts to previous. - Patch status.phase = rejected on application-scope plan → admission webhook rejects the patch (no reject for application scope per ADR 0027). **Зависит от:** 1.75 **Размер:** M --- ### 1.77 Application reconciler integration: gate pause/resume **Source:** ADR 0027. **Цель:** existing `Application` reconciler (delivered в Phase 1) теперь respects pending MigrationPlans — pauses child resource patching, sets status.phase=AwaitingMigrationApproval. **Поставка:** - [x] Update Application reconciler (`operator/operator-controllers/application/src/lib.rs`): - Before patching child resources (Deployment, Service), check for existing MigrationPlan в namespace `apprafter-system` with phase non-sealed (matches `pending-approval` | `approved` | `executing` | `failed` | empty; resumes on `completed` | `rejected`). Filter pulls scope.type=application AND scope.application.ref.{name,namespace} matching this app AND scope.application.environment matching ctx.env_name (skipped when env is None — wildcard). - If found: skip child patching, set `Application.status.phase = AwaitingMigrationApproval` + `Ready=False/MigrationPending` + `MigrationPending=True/MigrationPlanPending` (plan name in message). EndpointURL preserved (children still running prior version). Requeue 30s. - If no pending plan: continue normal reconcile. - Detection (`ApplicationMigrationStrategy::detect_destructive`) NOT invoked в B.1.77 reconcile — current v1alpha1 Application schema (image / replicas / expose / env) per spec.md §3.8 carries no destructive operations, so detect always returns None. Concrete fn signature `(old, new) -> Option` shipped on the strategy struct + `create_plan_for(...)` builder for future Phase 2.x callers wiring detection alongside `needs.*` / storage class / breaking image migration schema fields. - [x] Custom Argo CD health check (Lua script в argocd-cm ConfigMap via chart's `configs.cm.resource.customizations.health.apprafter.io_Application` key) for Application CR. Returns `Degraded` with the MigrationPlan name in the message when `Application.status.phase=AwaitingMigrationApproval` (reads `status.conditions[type=MigrationPending].message` for the verbatim text). Returns `Healthy` on `phase=Ready`. Surfaces в Argo CD UI as `Degraded` card. **Acceptance:** - User pushes destructive change в app repo (e.g., changes `needs.pg.selector`) → Argo CD syncs Application CR → reconciler creates MigrationPlan and pauses → Deployment continues running с prior version, Application UI shows Degraded with MigrationPlan reference. - Approve plan через `kubectl patch migrationplan -p '{"status":{"phase":"approved"}}' --type=merge --subresource=status` (или CLI/Backstage) → controller resumes, executes plan steps, Application reaches Ready. - User revert в Git → Argo CD syncs reverted spec → reconciler observes non-destructive → existing MigrationPlan superseded. **Зависит от:** 1.76 **Размер:** M --- ### 1.78 PlatformController MigrationPlan integration **Source:** ADR 0027. **Цель:** PlatformController detects destructive platform diffs, creates MigrationPlan instead of immediately patching umbrella Application. **Поставка:** - [x] Update PlatformController reconcile path (from 1.73): - [x] After computing diff and classifying, when classification != `safe`: - [x] Save current spec.pin в MigrationPlan **`spec.previousSpecSnapshot.pin`** (вместо `metadata.annotations[apprafter.io/previous-spec]` per plan.md placeholder — structured field из B.1.75 CRD schema preferred over annotation approach). - [x] Create MigrationPlan with scope.type=platform, scope.platform.components — pre-check by deterministic `platform--to-` name (idempotent); если plan exists с этим name → block bump regardless of classification. - [x] Skip patching umbrella Application; conditions UpgradeAvailable=True/BlockedByMigrationPlan + MigrationPending=True/ with plan name в message. - [x] On MigrationPlan approved: MigrationController executes → plan reaches `completed` → PlatformController's next reconcile sees plan completed (not blocking) → patches umbrella Application; Argo CD reconciles. - [x] On MigrationPlan rejected: PlatformMigrationStrategy.reject() (B.1.76, already implemented) reverts PlatformStack.spec.pin к `spec.previousSpecSnapshot.pin`. Same-transition retry blocked by rejected plan presence — operator must delete plan or pin к different target. **Acceptance:** - Publish platform-stack 0.3.0 (with breaking changes per compatibility metadata) → PlatformController creates MigrationPlan; PlatformStack.status.conditions[UpgradeAvailable]=True with "blocked by MigrationPlan". - Approve MigrationPlan → upgrade flows through. - Reject MigrationPlan → PlatformStack.spec.pin reverts; status reflects. **Зависит от:** 1.74, 1.76 **Размер:** S --- ### 1.79 CLI thin wrappers + `apprafter open` commands ✅ > v0.1.142 — sub-phase 1.79 shipped. `apprafter platform {status,upgrade}` + `apprafter migration {list,approve,reject}` + `apprafter open argocd` (port-forward + clipboard + browser launch) + npm-style version check + Argo CD UI Resource Action Lua scripts для MigrationPlan approve/reject. `platform {freeze,unfreeze,rescue}` rolled forward в 1.79a part 5 (closed v0.1.142). `platform channel` deferred к Phase 2 (multi-channel UX waits for stable/edge divergence); `open {backstage,grafana,hubble}` deferred к Tier 2+ (не tier-1 residents). **Source:** ADR 0025, 0026, 0027. **Цель:** CLI commands operating on declarative resources + UI access helpers + npm-style version check. **Поставка:** - [x] New CLI subcommands в `apprafter` binary: - [x] `apprafter platform status` — read PlatformStack.status, format человекочитаемо (current version, available, components healthy count, recent history). Implemented via kubectl shellout + `tabled` rendering (conditions + last-5 versionHistory). - [x] `apprafter platform upgrade [--to ]` — patch PlatformStack.spec.pin (или channel resolution if --to not specified). `--to ` pins; без `--to` clears `spec.pin` + flips `autoUpgrade=true`. - [ ] `apprafter platform channel ` — switch channel. **Deferred to 1.79a** — single-channel `stable` only ships в M1.5; multi-channel UX waits for Phase 2 where alternate channels actually exist. - [ ] `apprafter platform freeze [--version ]` — patch overrides..pin. **Deferred to 1.79a** — component-level pinning is a polish layer over the chart-level pin already shipped; ships alongside ResourceClaim CRUD в 1.79a. - [ ] `apprafter platform unfreeze ` — remove override. **Deferred to 1.79a** (paired с freeze). - [ ] `apprafter platform rescue` — reinstall Argo CD from loader (emergency recovery). **Deferred to 1.79a** — covered by `apprafter cluster-bootstrap --re-adopt` path that 1.79a's loader extensions formalise. - [x] `apprafter migration list` — list MigrationPlans, filter by phase/scope. Filters деференцированы (CLI list iterates ALL plans; phase/scope filtering trivial follow-up if operator demand surfaces). - [x] `apprafter migration approve ` — patch status.phase=approved. Status-subresource merge-patch via kubectl. - [x] `apprafter migration reject ` — patch status.phase=rejected (rejected by webhook for application scope; works for platform). Webhook denial message surfaces verbatim. - [x] `apprafter open ` — open browser to UI: - [x] `argocd` — `kubectl port-forward svc/argocd-server -n argocd 8080:443` + auto-fetch admin password from cluster secret + open https://localhost:8080. Cross-platform spawn (`xdg-open` / `open` / `cmd /c start`); blocks on child.wait() so Ctrl+C tears down the forward. - [ ] `backstage` — analogously. **Deferred to 1.79a / Tier 2+** — Backstage stack not tier-1 resident yet. - [ ] `grafana` — **Deferred Tier 2+**. - [ ] `hubble` — **Deferred Tier 2+**. - [x] npm-style CLI version check on every invocation: - [x] Cache в `~/.cache/apprafter/version-check.json` with 24h TTL. - [x] Fetch latest CLI release from `api.github.com/repos/apprafter/apprafter/releases/latest`. - [x] If newer: print warning line at start of output ("apprafter X.Y.Z available; you have ..."). Fail-quiet — network errors / GitHub rate-limit / JSON parse failures swallowed silently (debug log only); версия check is courtesy, not operational prerequisite. - [x] Argo CD Resource Action Lua script (added to argocd-cm ConfigMap via platform-stack chart): "Approve Migration" button on MigrationPlan resources в Argo CD UI. Discovery disables both Approve + Reject once `status.phase` leaves `pending-approval`; webhook denial of application-scope rejects surfaces в UI с the verbatim ADR 0027 message. **Acceptance:** - `apprafter platform status` outputs structured table within 2s. - `apprafter open argocd` opens browser with credentials filled in within 5s on second-run (cached password). - `apprafter migration approve ` succeeds; status updates within reconcile cycle. - CLI shows update warning when version stale. - Argo CD UI shows Approve button on MigrationPlan resources. **Зависит от:** 1.72, 1.75, 1.76 (CRDs must exist для thin wrappers) **Размер:** M --- ### 1.79a CLI app/repo subcommands + AppProjects + `open` polish ✅ > v0.1.142 originally; v0.1.160 + platform-stack 0.1.47 + argocd-cue-cmp v0.1.5 fully validated. Sub-phase shipped через 5 parts (AppProjects + project field; `open argocd` polish; `app {add,list,status,remove}`; `app {logs,rollback}`; `repo creds {add,list,show,rotate,remove}`) + `platform {freeze,unfreeze,rescue}` rollup. Real-cluster manual walk surfaced 12 walk-fixes (history rows below — #1 sync-wave/AppProject race, #2 standalone AppProject manifests at wave -30, #3 release-cli CUE install, #4 version_check tag stream picker, #5/#5b CMP entrypoint flat-stream + apprafter/ dir layout, #6 per-target state migration, #7 EXDEV cross-mount + richer-legacy preference, #8/#8b CMP discover glob→command + cue-cmp v0.1.4 rebuild, #9 channel-latest resolver decouples CLI from chart, #10 CMP discover stdout fix `grep -q` removed, #11 Namespace permitted в apps AppProject, #12 wizard defaults destination namespace к apprafter). Each walk-fix landed с regression-guard test; phase boundary tech-debt = zero. Deferred с rationale: interactive wizard для `repo creds add` (flag-driven sufficient, operator feedback gates); inline PAT prompt при private-repo cred miss (hint sufficient); API ping для PAT validation (format regex catches most copy-paste errors, adds flakiness); App CR conditions + pending MigrationPlan section в `app status` (Phase 2 destructive change detection prerequisite); last-used / expires в `repo creds list` (Argo CD / GitHub не surface'ят данные); `open backstage` (Tier 2+); URL pre-fill username `?username=admin` (Argo CD 7.7.7 не поддерживает — negative result). **Source:** ADR 0025, 0026 (Argo CD projects model); продолжение 1.79. **Цель:** убрать необходимость заходить в Argo CD UI для повседневных операций (добавление repo, deploy status, rollback) и разделить платформенные приложения от пользовательских визуально и через RBAC. #### Поставка — AppProjects в platform-stack chart - [x] Добавить три `AppProject` ресурса в umbrella chart (через `_loaderValues.argocd.values.configs.projects`, а не отдельную папку — Argo CD chart 7.7.7 сам создаёт AppProjects из этого block'а): - [x] `platform` — для core platform components. `sourceRepos: ["*"]`, `destinations: [{namespace: "*", server: "https://kubernetes.default.svc"}]`, `clusterResourceWhitelist: [{group: "*", kind: "*"}]`, `namespaceResourceWhitelist: [{group: "*", kind: "*"}]` (открыто на M1.5 — RBAC enforcement через AccessGrant приедет в Phase 4). - [x] `platform-providers` — для ServiceProvider operators (CNPG, Dragonfly, NATS, Kamaji). Те же permissions что и `platform`, разделение чисто визуальное + lifecycle-категория. Project заводится сейчас (а не лениво в Phase 2), чтобы UI selector показывал его сразу после bootstrap'а. - [x] `apps` — для user applications. `sourceRepos: ["*"]` (пока не введён RBAC через AccessGrant Phase 4), `destinations: [{namespace: "*", server: "https://kubernetes.default.svc"}]`, `clusterResourceWhitelist: []`, `namespaceResourceWhitelist: [{group: "apprafter.io", kind: "Application"}, {group: "", kind: "ConfigMap"}, {group: "", kind: "Secret"}, {group: "gateway.networking.k8s.io", kind: "HTTPRoute"}]`. - [x] Update umbrella Helm templates — все chart-managed Applications получают `spec.project: {{ default "platform" $component.project }}` через новое поле `#Component.project: string | *"platform"`. Default = `platform`; tier overlays / ServiceProvider charts могут override на `platform-providers` per-component. CLI loader's root platform Application также переехал на `spec.project: platform` (`cluster_bootstrap::render_root_application`). - [x] CMP plugin (`argocd-cue-cmp`) рендерит user Application CRs с `spec.project: apps` по умолчанию. **Закрыто:** `apprafter app add` wizard (`app_wizard.rs`) defaults parent Argo CD Application's `spec.project` к `apps` (clap default), а CMP-rendered user manifests (apprafter.io/Application) сами не несут `spec.project` (это Argo CD-specific поле, не AppRafter); end-to-end проверено walk-fix #10/#11/#12 на real cluster — landing-web/landing-cms apps зарегистрированы в Argo CD под project `apps`. #### Поставка — `apprafter open` polish - [x] `apprafter open argocd` URL → `/applications?proj=apps` по умолчанию. - [x] Флаги `--project ` (default `apps`) и `--all-projects` (убирает фильтр). Конфликтуют через `conflicts_with = "project"`. - [x] Output формат при открытии: ``` $ apprafter open argocd Opening Argo CD UI... URL: https://localhost:8080/applications?proj=apps Username: admin Password: H7x4kP9aB3... (copied к clipboard) ✓ Browser opened ℹ Press Ctrl+C к stop port-forward ``` - [x] Password copy to clipboard через `arboard` crate (cross-platform). Fail-quiet — headless / no-clipboard envs показывают `(clipboard unavailable — copy manually)` без error'а. - [x] Password также печатается в terminal в plaintext — юзер может подсмотреть если clipboard засрался другим контентом. - [ ] Попытка pre-fill username через URL `?username=admin` — Argo CD UI это не поддерживает (проверил empirically на 7.7.7); оставили только display + clipboard. **Закрыто negative-result'ом.** - [ ] Аналогичная обработка для `apprafter open backstage` (когда появится). **Deferred к Tier 2+** — Backstage не tier-1 resident. #### Поставка — `apprafter app` подкоманды - [x] `apprafter app add []`: - [x] Без аргумента: детектит git origin из cwd через `git remote get-url origin`, нормализует (SSH→HTTPS, убирает `.git`). - [x] Флаги: `--name ` (default = repo name), `--branch ` (default = current branch для cwd-режима, `main` для explicit URL), `--path ` (default `/`), `--project ` (default `apps`), `--remote ` (default `origin`), `--no-ping` (skip reachability check). - [x] Interactive: спрашивает name/branch/path с дефолтами; non-interactive: использует defaults или fails если `--git-url` не задан. **Закрыто v0.1.145** через `cli/platform-cli/src/commands/app_wizard.rs` (Text+Select prompts, cwd-detect для path/branch/origin, inline DNS-1123 валидация); v0.1.160 (walk-fix #12) добавил шестой prompt «Destination namespace» с DNS-1123 валидацией. `--no-interactive` opt-out для CI/headless. - [x] Проверка доступности репо — `git ls-remote` через subprocess (поддерживает HTTPS auth check без token, для private — детект unauthorized). `--no-ping` для air-gapped CI. - [ ] Если репо private и не зарегистрирован cred — inline prompt: "Use existing PAT / Add new PAT / Skip". **Deferred к v0.1.141** — лендится вместе с `apprafter repo creds add`. Сейчас auth failure surfaces hint pointing к `apprafter repo creds add`. - [x] Пишет Argo CD `Application` CR в `argocd` namespace с label `apprafter.io/managed-by: apprafter` и annotation `apprafter.io/source: cli`. CR указывает на пользовательский repo, CMP plugin рендерит `apprafter/Application.cue` оттуда. - [x] `apprafter app list [--project ] [--all-projects]`: - [x] Default filter `--project apps`. - [x] Таблица: name, project, repo, revision, sync state, health. (last sync time не surfaced — Argo CD CR не carry'ит human-friendly timestamp в `status.sync`; добавим если operator feedback потребует). - [x] `--all-managed` toggle drops the managed-by label filter. - [x] `apprafter app status `: - [x] Detail view: Argo CD Application sync/health + source + destination + recent revisions (last 3 из `status.history`). - [x] AppRafter Application CR conditions (если CMP уже отрендерил) + перечень child resources. **Закрыто v0.1.164** через `apprafter app status --resources`/`-r` flag — рендерит Argo CD's `status.resources[]` (NAME/KIND/NAMESPACE/STATUS/HEALTH) + Pods в destination ns через operator's `app.kubernetes.io/name=` label (READY/STATUS/RESTARTS/AGE с kubectl-style waiting.reason heuristic — surfaces ImagePullBackOff / CrashLoopBackOff / etc. что Argo CD app-level Healthy aggregation скрывает). Pod fetch non-fatal — соблюдает Argo CD's view как authoritative для sync/health. Operator-side health propagation (CR status.phase отражающий actual Pod state) остаётся Phase 2/3 concern (ResourceClaim wait semantics). - [ ] Если есть pending MigrationPlan для этого app — выводит в верхней секции с approve-командой. **Deferred к v0.1.140 / Phase 2** — нужны user-app MigrationPlans из Phase 2 destructive change detection. - [x] `apprafter app logs [--follow] [--tail ] [--container ] [--pod ]`: - [x] Wrapper над `kubectl logs` с label selector `app.kubernetes.io/instance=` (Argo CD's documented standard label, stamped на every child resource). - [x] Multi-pod: aggregate by default через selector form с `--prefix=true` + `--max-log-requests=10`; `--pod ` narrows к single pod (selector mode skipped). - [x] Pure helpers `build_kubectl_logs_target` + `build_kubectl_logs_args` exhaustively tested без spawning kubectl. - [x] `apprafter app rollback [--to ]`: - [x] Без `--to`: rollback к предыдущей Git revision (читает `status.history` из Argo CD Application через pure `pick_previous_revision` helper — chronologically ordered list, second-to-last entry). - [x] С `--to `: rollback к указанному коммиту. - [x] Внутри — patch `spec.source.targetRevision` через JSON merge-patch, Argo CD ресинкает на следующем reconcile cycle. - [x] Confirmation prompt в interactive (`inquire::Confirm` default No), `--yes` для non-interactive. - [x] Pre-flight refuse когда target revision matches current (no-op). - [x] `apprafter app remove `: - [x] Confirmation prompt через `inquire::Confirm` (default No), `--yes` для non-interactive. - [x] Удаляет Argo CD Application через `kubectl delete`, в каскаде — child resources (Argo CD reconciles via ownerRefs). - [x] `--keep-data` опция — flips `syncPolicy.automated.prune: false` ДО delete, child resources (PVC/ResourceClaims) сохраняются. **Alias:** [x] `apprafter a` → `apprafter app` (проверил — `apprafter apply` не конфликтует с `a` потому что clap резолвит alias строго; `apprafter a add` работает, `apprafter a apply` не существует). #### Поставка — `apprafter repo creds` подкоманды - [x] `apprafter repo creds add `: - [ ] Interactive wizard для всех полей. **В v0.1.141 flag-driven only** (`--url-prefix` required, `--type pat` default, `--username git` default); token reads из stdin через `inquire::Password` masked когда `--token` opused и stdin = TTY. Wizard приедет если поступит реальный operator feedback. - [x] Token validation: - [x] GitHub: `github_pat_*` (fine-grained, 80+ char body) или `ghp_*` (classic, 40 chars total), regex check. **API ping deferred** — adds network round-trip и failure modes (rate limiting, transient network); validation gates на shape suffices для most copy-paste errors. - [x] GitLab: `glpat-*`, 20+ char body length check. - [x] Generic fallback: 20+ chars (Gitea/Codeberg/Forgejo); `--no-validate` для bypass. - [x] Basic auth: any non-empty password accepted. - [x] Создаёт k8s Secret в namespace `argocd` с labels: - [x] `argocd.argoproj.io/secret-type: repo-creds` - [x] `apprafter.io/managed-by: apprafter` - [x] `apprafter.io/cred-name: ` - [x] `stringData` (НЕ `data` — kubectl base64-encodes server-side): `url`, `username`, `password`. - [x] Annotation `apprafter.io/auth-type: ` чтобы `rotate` мог re-validate against original auth type. - [x] `apprafter repo creds list`: - [x] Таблица: name, URL prefix, type, username. **last-used / expires deferred** — Argo CD не stamps usage timestamps на the Secret и GitHub fine-grained PATs don't expose `exp` через token shape; будем surface когда appear полезный signal. - [x] `apprafter repo creds show `: - [x] Detail view, password замаскирован (`****`) + pointer к `kubectl get secret -o jsonpath='{.data.password}' | base64 -d` для plaintext decode когда нужно. - [x] `apprafter repo creds rotate `: - [x] Prompt только для нового token, остальные поля сохраняются. - [x] Patch existing Secret через JSON merge-patch (не пересоздаёт — repo-server caches resourceVersion). - [x] Re-validation token'а перед patch против recorded `apprafter.io/auth-type` annotation. - [x] `apprafter repo creds remove `: - [x] **Dependency check** by default — refuses когда есть Argo CD Applications с `spec.source.repoURL` starting with the creds' `url` field. Pure helper `find_apps_matching_prefix` walks the Application list filter testable без cluster. - [x] `--force` skips dependency check (для migrations к а different creds entry). - [x] `--yes` skips только confirmation prompt (still runs dependency check). #### Поставка — context-aware error hints - [x] При `apprafter app add` без `.git` в cwd: hint "не удалось запустить git remote get-url ... Запусти из git-репозитория или передай URL явно через `apprafter app add `." - [x] При попытке `app add` для private репо без creds в non-interactive: error "git ls-remote отказал в доступе ... Зарегистрируй creds через `apprafter repo creds add` и повтори `apprafter app add`." - [x] При попытке `app add` с конфликтным именем (Application с таким name уже есть): error "Application '' уже зарегистрирован в namespace argocd. Запусти `apprafter app status ` чтобы посмотреть текущее состояние, или `apprafter app remove ` для каскадного удаления, либо передай другой `--name`." #### Поставка — `apprafter platform` extension (закрывает остатки от 1.79) - [x] `apprafter platform freeze [--version ]` patches `PlatformStack.spec.overrides..pin`. Без `--version` reads effective version из `status.componentVersions.` и locks that. CRD schema (`schemas/v1alpha1/platformstack.cue`) already supports `overrides` map (B.1.74 era). - [x] `apprafter platform unfreeze ` — RFC 7396 merge-patch с null value strips `overrides.` entry. Component falls back к chart's curated pin. - [x] `apprafter platform rescue [--yes]` — thin wrapper over `apprafter cluster-bootstrap` с recovery confirmation banner. Use case: Argo CD self-adopt stuck (stale chart, corrupted ConfigMap, pod-eviction loop). #### Acceptance - [x] `apprafter open argocd` открывает UI с фильтром `apps`, username отображается в выводе, password в clipboard. - [x] В Argo CD UI верхний project selector показывает три проекта (+ legacy `default`); `apps` пустой при свежем bootstrap, `platform` и `platform-providers` содержат соответствующие Applications. - [x] `cd && apprafter app add` без аргументов корректно детектит origin и регистрирует app. - [ ] `apprafter app add` для private репо без creds → **hint pointing на `apprafter repo creds add`** (inline interactive prompt deferred — flag-driven flow + hint sufficient). - [x] `apprafter repo creds add` с невалидным GitHub PAT → fail с regex error до API call. - [ ] `apprafter repo creds add` с валидным форматом но revoked token → **API ping deferred** (см. above — adds network round-trip + flakiness; format validation alone catches most copy-paste errors). - [x] `apprafter app rollback ` без `--to` откатывает к предыдущей revision; Argo CD синкает в течение reconcile cycle. - [x] `apprafter app remove` удаляет Application каскадно, `--keep-data` сохраняет PVC. - [x] `apprafter repo creds rotate` обновляет token, existing apps продолжают синкаться без даунтайма Argo CD repo reconcile. #### Не входит в этот item - AccessGrant / RBAC enforcement через AppProject (Phase 4 целиком). - Reverse proxy для `apprafter open` (отдельный item, после M2, когда понадобится Backstage с теми же проблемами). - `apprafter app scale`, `apprafter app env set` — высокоуровневые ops-команды (M2+, после ServiceProvider/ResourceClaim). - Backstage Application plugin — отдельный item в Phase 3. **Зависит от:** 1.79 (CLI thin wrappers infrastructure + `open` для argocd базовый). **Размер:** M --- ### 1.79b CLI app ergonomics — `app open` + scaffolding + runtime templates ✅ > v0.1.174 + platform-stack 0.1.48 + argocd-cue-cmp v0.1.6 — sub-phase 1.79b shipped через parts 1–3b: `apprafter app open ` (part 1, v0.1.161 — port-forward + Service-resolution + browser launch + graceful Ctrl+C), runtime detection primitives (part 2, v0.1.166 — filesystem-marker → runtime mapping с High/Medium/Low/Fallback confidence), `apprafter app scaffold` + embedded `.cue.hbs` template engine (part 3, v0.1.167) + scaffold wizard в `app add` step 0 + interactive runtime picker (part 3b, v0.1.168). Real-cluster manual walk surfaced walk-fixes (post-B.1.79b #1–#4 v0.1.162–165: app-open label-resolution off operator's `app.kubernetes.io/name` + kubectl stderr surfacing + `app status --resources` child workload state + Cargo.lock/bun-smoke sync; post-Part-3b #1–#6 + #11 v0.1.169–174 + chart 0.1.48 / cue-cmp v0.1.6: scaffold UX/SPDX/namespace/cred-hint polish, `spec.source.path` absolute-path fix, CUE-import drop, schema inline→vendored CUE module под `apprafter/cue.mod/`, image-ref derived from git origin, CMP entrypoint cds into package dir). Each walk-fix landed с regression-guard test; phase boundary tech-debt = zero. Design divergences (приняты): template engine ships 2 consolidated `.cue.hbs` (default + blank) вместо 12 per-runtime files (port defaults baked into `defaults_for`); multi-stack default = first-High-in-order. Deferred с rationale (Part 4, ещё не отгружено): `examples/applications/` reference manifests, `docs/user-guide/cli/app-scaffold.md`, quickstart update, `spec.resources`/`spec.healthcheck` template fields (v1alpha1-only сейчас), `--lang ru|en` template comments (English-only). **Source:** Продолжение 1.79a. Closes gaps в quickstart flow для unfamiliar users. **Цель:** убрать k8s complexity из первого запуска приложения. Юзер не должен знать про port-forward / kubectl, и не должен с нуля писать `apprafter/Application.cue` если у него стандартный stack. #### Поставка — `apprafter app open ` - [x] Wrapper над `kubectl port-forward` для пользовательского app'а: - Резолвит Application name → Service в namespace (через AppRafter Application CR labels). - Определяет primary port из Application.expose.port или Service.spec.ports[0]. - `kubectl port-forward svc/ 8080:` в background process. - Открытие `http://localhost:8080` в браузере через `open` (Linux/Mac) / `start` (Windows) / `xdg-open`. - Output формат: ``` $ apprafter app open my-parser Forwarding to my-parser on namespace 'default'... Service: my-parser Port: 3000 (container) → 8080 (localhost) URL: http://localhost:8080 ✓ Browser opened ℹ Press Ctrl+C to stop port-forward ``` - [x] Флаги: - `--port ` — override local port (default 8080, со shift на 8081/8082/etc. если занят). - `--container-port ` — override container port если app exposed multiple. - `--no-browser` — только port-forward, без открытия браузера (для CI/scripts). - [x] Error handling: - App не найден → "Application '' not found. List with `apprafter app list`." - App not Healthy → warning "Application is in state ; port-forward may fail. Continue anyway? [y/N]". - Local port занят → auto-increment до 8090, дальше error. - kubectl missing → error с hint к target add. - [x] Graceful shutdown по Ctrl+C — kill port-forward, exit clean. #### Поставка — Runtime detection в `apprafter app add`/`scaffold` - [x] Detection heuristic из cwd: | Маркер | Detected runtime | Priority | |---|---|---| | `bun.lock` или `bun.lockb` | `bun` | High | | `pnpm-lock.yaml` | `node-pnpm` | High | | `yarn.lock` | `node-yarn` | High | | `package-lock.json` | `node-npm` | High | | `package.json` без lock-файла | `node-npm` (fallback) | Medium | | `pyproject.toml` с `[tool.poetry]` | `python-poetry` | High | | `pyproject.toml` с `[tool.uv]` или `uv.lock` | `python-uv` | High | | `Pipfile` | `python-pipenv` | High | | `requirements.txt` без других | `python-pip` | Medium | | `Cargo.toml` | `rust` | High | | `go.mod` | `go` | High | | `Dockerfile` без других маркеров | `docker` (build-only template) | Low | | Ничего | `blank` (пустой шаблон с TODO) | Fallback | - [x] Confidence levels: - `High`: явный lock-файл = runtime установлен, версии воспроизводимы. Default-select без вопросов в auto-mode. - `Medium`: маркер есть, но lock отсутствует — менее уверенно, confirm prompt всё равно. - `Low` / `Fallback`: prompt со списком всех runtimes, юзер выбирает руками. - [x] Multiple маркеры одновременно (monorepo, мультистек): - Если детектится 2+ runtime с High confidence — prompt со списком, default = первый по алфавиту. - Можно подсказать через CLI флаг `--runtime ` сразу для non-interactive. #### Поставка — Application.cue templates - [x] Template engine: Handlebars-like substitution на статичных `.cue.hbs` файлах в CLI binary через `include_str!` (embedded в release). - [x] Variables в шаблоне: - `{{app_name}}` — из cwd name или CLI flag. - `{{image_ref}}` — placeholder `ghcr.io//:latest` для пользовательской подстановки. - `{{primary_port}}` — defaults per runtime (bun/node: 3000, python: 8000, rust/go: 8080, docker: 8080). - `{{healthcheck_path}}` — default `/health` со комментарием "// adjust if your app uses different path". - [x] Шаблоны (`cli/templates/application/*.cue.hbs`): - `bun.cue.hbs` — runtime=bun, build с bun build script. - `node-pnpm.cue.hbs`, `node-yarn.cue.hbs`, `node-npm.cue.hbs` — соответствующий package manager в build steps. - `python-poetry.cue.hbs`, `python-uv.cue.hbs`, `python-pipenv.cue.hbs`, `python-pip.cue.hbs`. - `rust.cue.hbs` — cargo build --release. - `go.cue.hbs` — go build, static binary. - `docker.cue.hbs` — assume Dockerfile в корне, no build steps generation (юзер сам Dockerfile управляет). - `blank.cue.hbs` — пустой Application с TODO-комментариями на все required поля. - [x] Каждый шаблон содержит: - `apiVersion`, `kind`, `metadata.name` - `spec.image` — placeholder - `spec.expose` — minimal (port + comment "set public: true и hostname when ready to expose") - `spec.resources` — sensible defaults (100m CPU / 128Mi RAM с комментарием "tune based on observed usage") - `spec.healthcheck` — default path `/health` с note про customization - Inline-комментарии на русском или английском объясняющие что каждое поле делает (selectable через `--lang ru|en`, default `en`) #### Поставка — Scaffold flow в `app add` и `app scaffold` - [x] `apprafter app add` (расширение из 1.79a): - Шаг 0 (before any prompts): проверка наличия `apprafter/Application.cue` в cwd. - Если **отсутствует**: - Run runtime detection. - Wizard prompt: "No `apprafter/Application.cue` found. Generate one? [Y/n]". На "Y" — continue scaffolding. - List с pre-selected default = detected runtime. Юзер может выбрать другой. - Запрос на app name (default = repo dir name). - Confirm: "Generate `apprafter/Application.cue` and `.gitignore` entries?". - Создание файла из template, append `.apprafter/local/` в `.gitignore` если отсутствует (для будущих local secrets). - Print "✓ Created apprafter/Application.cue — review and adjust before committing". - Если **присутствует**: пропускает scaffold step, идёт к existing flow (`git remote get-url origin` → register Application). - В non-interactive mode без `--scaffold`: если файл отсутствует — fail с hint к `apprafter app scaffold`. - [x] `apprafter app scaffold` — standalone команда для re-scaffolding или explicit-only flow: - Без аргументов: detection + interactive wizard, генерит файл. - Флаги: `--runtime ` (override detection), `--name `, `--lang ru|en` (комментарии), `--force` (overwrite existing). - Без `--force` отказывается перезаписать existing `apprafter/Application.cue` — exit code 2. #### Поставка — README updates + examples directory - [ ] `examples/applications/` в monorepo — реальные working Application.cue для каждого preset (используются как fixture для тестов template engine + reference в docs). **Deferred — Part 4 (ещё не отгружено).** - [ ] `docs/user-guide/cli/app-scaffold.md` — описание templates, runtime detection logic, customization guide. **Deferred — Part 4 (ещё не отгружено).** - [ ] Update `docs/operator-guide/quickstart.md` — шаг "Write Application.cue" заменён на "Run `apprafter app add` — it scaffolds for you". **Deferred — Part 4 (ещё не отгружено).** #### Acceptance - [x] `cd && apprafter app add` детектит bun, генерит `apprafter/Application.cue` с bun preset, регистрирует Argo CD Application. - [x] `apprafter app open ` после `apprafter app status` показывающего Healthy → port-forward работает, browser открывается на app'е. - [x] `apprafter app open` для app в Pending/CrashLoopBackOff показывает warning, но всё равно делает port-forward по confirmation. - [x] Multiple маркеры (e.g., python-poetry + Dockerfile) → prompt со списком, не auto-pick. - [x] `apprafter app scaffold` для пустого репо генерит `blank` template с TODO-комментариями. - [x] `apprafter app scaffold` для существующего `Application.cue` без `--force` → exit 2. - [x] Generated `apprafter/Application.cue` проходит `cue vet` (валидный CUE). - [ ] Generated файл с `image` placeholder + push в репо → Argo CD синкает; AppRafter Application reaches `OutOfSync` / `Suspended` until юзер обновит image. Status condition объясняет что делать. **Partial** — scaffold + push + Argo CD sync через CMP shipped (ImagePullBackOff на placeholder-image видно через `apprafter app status --resources`, walk-fix #3); operator-side `Suspended` phase + explanatory status condition остаётся Phase 2/3 (CR health-propagation, как app-status в 1.79a). #### Не входит в этот item - Auto-generation Dockerfile для runtime presets (отдельный future item, нетривиально и небезопасно). - Backstage Software Templates integration (Phase 3, Backstage plugin). - Live `cue vet` + linting в CLI на каждом save (Phase 3+). - AI-assisted Application.cue refinement (вне scope OSS платформы). **Зависит от:** 1.79a (`apprafter app add` базовый flow, AppProjects, repo creds). **Размер:** S-M --- ### 1.79c Private-repo credential flow — `SourceCredential` CRD > 🏁 SR: A · order 3 — private-repo credential flow (`SourceCredential`, ADR 0039); **must follow the 2.11 SealedSecrets controller+seal slice** в SR-порядке (cross-phase dep — phase-номера немонотонны: Phase-1 item выполняется после Phase-2 slice, это ожидаемо; см. `speedrun-plan.md` §4.2). > > ✅ **ЗАКРЫТО** (walk пройден чисто 2026-06-01, без багов; запушено, образы opera­тора v0.1.137 + platform-stack 0.1.51 опубликованы, кластер сошёлся). S0–S5 + acceptance #1/#2/#3/#5/#6 валидированы на живом кластере. **Остаётся только осознанно отложенное** (не долг, cross-phase): acceptance #4 live-wiring (admission создаёт MigrationPlan + pause на host-removal) co-отложен с application-scope **B.1.77**; config-repo (GitOps) delivery → **Phase-3 Backstage**. Работа велась на `master`, design+plan в `docs/superpowers/{specs,plans}/2026-05-30-1-79c-*`. S0–S4 ВАЛИДИРОВАН end-to-end на живом кластере: `repo creds add`→seal→derive, `app add`→Argo клонит приватный репо→под тянет приватный ghcr-образ (1-й acceptance), `app remove`→Argo каскадит CR→оператор+ownerRef убирают workload без зависания. Walk-fixes #1 (seal-label `/`), #2 (registry-host lowercase), #4 (operator `deletionTimestamp`-guard), #6 (Argo cascade finalizer name — `resources-finalizer`, не `-finalization`); тупиковые #3/#5 откачены в #6. **S5 поставлен** (validity-probe · coverage-gate `confirmed` · derived-Secret GC finalizer · scoped CLI RBAC seed · `detect_destructive` классификатор) одним координированным релизом. Версии: CLI `0.1.184` (тег `v0.1.184` локально) · operator `v0.1.137` · platform-stack `0.1.51`. **Ждёт:** пользователь пушит master → CI публикует operator/webhook образы + platform-stack чарт → auto-update кластера сходится → ручной walk (acceptance #2/#3/#4 + validity + GC; см. инструкции в ответе ассистента). **spec.md уже актуализирован** (Revision 9: §3.12 SourceCredential, §4.5 деривация+validity, §4.7, CLI-секция — описывает весь ADR 0039 включая S5-поведения; новой правки не требует — план-заметка отставала). > **S0 ✅** (v0.1.175 + platform-stack component, chart-only): sealed-secrets controller компонент (bitnami 2.18.6, ns `apprafter-system`, `fullnameOverride` пинит Service) + **нативный Rust-seal** в `cli-providers::k8s::sealing` (RSA-OAEP-SHA256 ⊗ AES-256-GCM single-use, strict scope, bitnami wire-format; cert fetch через kube API service-proxy `KubectlRunner::get_raw`) + команда `apprafter secret seal`. Вынесенный вперёд prereq-slice из 2.11. > **S1 ✅** (v0.1.176): `SourceCredential` CRD в 3 слоя — CUE schema + example; kube-rs тип (per-half status conditions); OpenAPI v3 CRD (sync-wave -5); admission `validator_sourcecredential.rs` (at-least-one-of git/registry, exactly-one backend, non-empty coverage) + dispatch + integration-тесты; bootstrap CRD-Established wait. > **S2 ✅** (operator-only): новый крейт `operator-controllers/sourcecredential` — watch SourceCredential, read unsealed material, derive prefix-matched Argo `repo-creds` (argocd ns) + status `GitPresent`/`GitValid=Unverified`; field manager `apprafter-sourcecredential`; RBAC (sourcecredentials + secrets); wired 4-м контроллером в `main.rs`. > **S3 ✅** (operator-only): registry-половина — canonical `dockerconfigjson` (apprafter-system) + `RegistryPresent`/coveredHosts; **Seam A** — Application-контроллер host-match rendered image → SSA-копия pull-secret в app ns → `Deployment.imagePullSecrets`. Pure matching в `application/src/pull_secret.rs`. > **S4 ✅** (v0.1.177): CLI `repo creds` → thin front-end над SourceCredential. `add` seal + SealedSecret + CR (git всегда; ghcr.io/ инферится для github); `list`/`show` читают `.status` (никогда материал); `rotate` re-seal; `remove` delete CR+SealedSecret за reverse-dep gate; `app add` coverage-hint читает SourceCredential. Legacy raw repo-creds флоу cut over (без shim). > **S5 ✅ поставлен** (operator v0.1.137 / platform-stack 0.1.51 / CLI v0.1.184): `SourceCredentialMigrationStrategy.detect_destructive` (классификатор coverage-removal→breaking + 7 тестов + экспорт; live plan-creation wiring co-отложен с B.1.77) · live validity-probe (представительный репо/образ из матчащегося Argo/AppRafter Application → git smart-HTTP / scoped registry v2 token-exchange; консервативный mapping, egress-blocked → `Unverified`, не `Invalid`; `lastValidated`) · coverage-gate `present`|`confirmed` (`app add --coverage-gate`) · derived-Secret GC finalizer (`apprafter.io/derived-secrets-cleanup`, cross-ns без ownerRef-каскада; RBAC `delete` на secrets) · scoped CLI credential-author Role seed (unbound, no read на derived Secrets). config-repo delivery — **DEFER** (→ Phase-3 Backstage). Запись в `plan-history.md` добавлена. > ⚠️ **Release-координация — СДЕЛАНО:** координированный bump одним коммитом `chore(release): publish 1.79c S5` (тег `v0.1.184`): operator+webhook `Chart.yaml` appVersion v0.1.136→**v0.1.137** + chart version v0.1.117→v0.1.118; platform-stack component-пины следом; `currentVersion` 0.1.50→**0.1.51** + `safe` compatibility-запись; `RELEASED_OPERATOR_VERSION` деривится build.rs из appVersion (проверено: v0.1.137); CLI v0.1.183→**0.1.184**. `operator/v0.1.137` + `platform-stack/v0.1.51` — воркфлоу-managed на push. Ждёт `git push` пользователя. **Source:** ADR 0039. Продолжение 1.79a (repo/app subcommands). Новый item — не затрагивает отгруженный 1.79b (`app open`/scaffolding/runtime templates сохраняют свой номер и историю v0.1.161–v0.1.174). **Цель:** один безопасный credential-флоу к приватным client-репозиториям — git-read для Argo CD + registry-pull для kubelet — из одного источника, без сырых ungated-ресурсов и без plaintext-секретов. Закрывает три дефекта текущего 1.79a-флоу: **(1)** CLI создаёт raw Argo `Application` + raw `repo-creds` Secret в обход admission+оператора (credential change не классифицируется как потенциально деструктивная операция); **(2)** credential лежит plaintext в kube Secret (несовместимо с SealedSecrets 2.11 / ADR 0024 Layer 2); **(3)** pull-secret не заводится вообще — приложения с приватным registry не стартуют. #### Поставка — `SourceCredential` CRD (config-only, ноль материала) - [x] CUE schema `schemas/v1alpha1/sourcecredential.cue`: `spec.git? { backend, repoPrefixes: [...] }`, `spec.registry? { backend, hosts: [...] }`; `#Backend = { sealedSecretRef } | { openBaoPath }`. Обе половины независимы; single-PAT кейс = один backend в обоих. В `spec` — никакого токена/base64. - [x] CRD OpenAPI v3 + admission webhook (cross-field: хотя бы одна из git/registry; непустые prefixes/hosts; валидный backend ref). - [x] `status.conditions` per-half: `Present` / `Valid` / `Invalid` / `Unverified` + covered prefixes/hosts + `lastValidated`. #### Поставка — operator derivation - [x] git → **prefix-matched** Argo `repo-creds` Secret в `argocd` ns (Argo клонит по URL-prefix; derived output, не hand-managed). - [x] registry → static `dockerconfigjson` pull-secret в workload ns; **auto-attach** к workload SA / `Deployment.imagePullSecrets` по **registry-host match** (`image: ghcr.io/...` → `SourceCredential` с host `ghcr.io/...`). - [x] Reconcile держит derived Secrets консистентными с CR + материалом (single source of truth; `rotate` материала → передеривка обеих производных). - [x] RBAC оператора: read `SourceCredential`, read unsealed material, write argocd repo-cred + workload pull-secret, patch SA/Deployment. #### Поставка — operator validation + status - [x] git validity: smart-HTTP `GET /info/refs?service=git-upload-pack` (Basic auth, reqwest) против **представительного репо** из матчащегося Argo Application (префикс — орга, нужен конкретный объект; решение в `validity.rs`). registry validity: scoped v2 token-exchange (oci-distribution) против представительного образа из матчащегося AppRafter Application. On-change + каждые 60s (1/min — в пределах GitHub rate-limit). Консервативный mapping: Valid только на 2xx/success, Invalid только на явный auth-reject (401/403 / auth-failure), всё неоднозначное → Unverified. - [x] Egress-blocked → `Unverified` (не `Invalid`) — network-error никогда не даёт Invalid. `GitValid`/`RegistryValid` + `status.lastValidated` пишутся. Нет матчащегося app → тоже `Unverified` (нечего пробить). - [x] Coverage-gate конфигурируем `present` | `confirmed` (`app add --coverage-gate`, ValueEnum, default `present`): `present` — warn-only post-registration по наличию prefix (как было); `confirmed` — pre-flight БЛОКИРУЕТ регистрацию приватного (`https`) репо, пока покрывающий `SourceCredential` не в `GitValid=True` (pure `valid_credential_covers` читает `.status.conditions`). Egress-restricted → дефолт `present` (validity остаётся `Unverified`). [ADR 0039 §Validation and status] #### Поставка — CLI front-end refactor (`repo creds` → `SourceCredential`) - [x] `apprafter repo creds add`: shape-check (existing regex) → seal материал client-side (kubeseal публичным сертом контроллера; серт **пинится** / fetch через TLS kube API) → create/update `SourceCredential` CR + SealedSecret. Опц. поллит `.status` несколько сек для validity-фидбэка; иначе «submitted, validity pending». - [x] `list` / `show`: читают `.status` (coverage + validity), **никогда** материал (CLI не может расшифровать SealedSecret — нет cluster private key). - [x] `rotate`: re-seal материал на эквивалентный валидный cred (оператор передеривает обе производные; non-destructive). - [x] `remove`: delete CR с reverse-dep gate (переиспользует `find_apps_matching_prefix` из 1.79a). - [x] `app add` coverage-check: гейтит по «есть `Valid` cred, покрывающий repo prefix» из `.status` (вместо CLI-догадки); режим гейта = coverage-gate (`present`/`confirmed`) из operator-секции выше. - [x] Scoped CLI RBAC role (Phase-4 scoped-identity seed, фиксируется уже сейчас): `Role` `-credential-author` в release-ns (`operator/charts/.../templates/rbac-cli.yaml`, gate `.Values.cliRole.create`), shipped **UNBOUND** (Phase 4 биндит к scoped identity). read `SourceCredential` (+`status`), write `SourceCredential` / `SealedSecret` (bitnami.com), **no read** на core/v1 Secrets — крипто-нечитаемость материала из CLI по построению, не только RBAC. Public-cert fetch через service-proxy — отдельный non-sensitive grant, не в seed. [ADR 0039 §CLI as a thin front-end] - [x] **Cut over** от legacy raw `repo-creds` Secret флоу — без migration shim (pre-1.0, флоу живёт только в dev-кластере). #### Поставка — MigrationPlan integration (destructive credential change) - [x] `SourceCredentialMigrationStrategy` с `detect_destructive(old, new) -> Option`: rotate-to-equivalent-valid = `None`; coverage removal (repo-prefix / registry-host, включая drop целой half) = destructive → `DestructiveChange{trigger:"coverage-removal", classification:"breaking"}`. Actor-agnostic классификатор поставлен + 7 юнит-тестов + экспорт. **Live plan-creation wiring** (reconcile-time snapshot→build MigrationPlan→pause derivation) + `create_plan_for` + scope-вариант `sourcecredential` в MigrationPlan CRD (kube-rs/OpenAPI/CUE/admission) **co-отложены** с application-scope live-wiring (B.1.77 — «один call-site, который раскомментируют»; ADR 0039 §169 определяет вклад 1.79c в gate именно как «adds a `MigrationStrategy.detect_destructive`»). `delete-while-referenced` (удаление CR при матчащихся приложениях) уже закрыт reverse-dep gate в CLI `repo creds remove` (S4); admission-side actor-agnostic вариант — с тем же B.1.77. Зависит от MigrationPlan CRD (1.72–1.78). #### Поставка — delivery modes - [x] CLI→cluster: `kubectl apply` SealedSecret + CR. - [~] config-repo (опц. инфра-репо): commit sealed + CR, Argo синкает (материал sealed → git-safe). Pure-GitOps-без-cluster-read: coverage-match по declared prefixes, validity в Backstage. **DEFER** (явное решение на закрытии 1.79c, 2026-05-31): помечен «опц.» в исходном scope; pure-GitOps-без-cluster-read (validity в Backstage) — заметный кусок, ближе к Backstage-фазе (Phase 3). Архитектурно уже поддержан (материал sealed → git-safe в обоих режимах — ADR 0039 §"Delivery is mode-agnostic"); закрытие 1.79c не требует. Подтянуть с Phase-3 Backstage-вью. #### Поставка — credential type - [x] Launch default: single classic PAT (`repo` + `read:packages`) в обе половины. GitHub-ограничение (нет `repo:read`-only; fine-grained без packages; App-токен ghcr.io не берёт) — принимается; платформа хранит sealed. - [x] Schema split-ready с первого дня (git ≠ registry backend). Wizard split (deploy-key/fine-grained git + `read:packages`-only registry с package-level access; GitLab — один `read_repository`+`read_registry` токен) — **opt-in, не дефолт**; визард-выбор откладывается до operator feedback. #### Acceptance - [x] Private GitHub репо + приватный ghcr.io образ: `repo creds add` (classic PAT) → `SourceCredential` `Valid` → `app add` проходит coverage-check → Argo клонит (prefix-matched repo-cred) → оператор рендерит Deployment с auto-attached pull-secret → под стартует, образ тянется. - [x] Org-cred (`repoPrefixes: ["github.com/myorg/"]` + `hosts: ["ghcr.io/myorg/"]`) покрывает второе приложение орги без отдельного `repo creds add` (auto-match); в `Application.cue` про credentials ничего. **Валидировано walk'ом 2026-06-01.** - [x] `repo creds rotate` на валидный новый PAT → обе производные передериваются, Argo + kubelet продолжают без даунтайма; MigrationPlan не создаётся (non-destructive; `detect_destructive`=`None`). **Валидировано walk'ом 2026-06-01.** - [~] Убрать registry-host из покрытия CR, пока приложение на него матчится → admission создаёт MigrationPlan; derived pull-secret не трогается до approve. **Классификатор поставлен** (`detect_destructive` → `coverage-removal`/`breaking`, 7 тестов), но **live admission-creates-MigrationPlan + pause co-отложен с B.1.77** (паритет с application-scope; ADR 0039 §169 — вклад 1.79c = «adds `detect_destructive`»). Сейчас удаление host'а проходит, derived pull-secret передеривается под reverse-dep gate CLI; полное end-to-end gating — с B.1.77. - [x] `repo creds show` / `list` / `kubectl get sourcecredential -o yaml` — нигде plaintext токена; SealedSecret нерасшифровываем без cluster private key. - [x] Restricted-egress кластер: валидный PAT → status `Unverified` (не `Invalid`); coverage-gate в `present`-режиме пропускает. #### Не входит в этот item - OpenBao backend (T2) — с 2.7–2.8 / 3.11; schema `backend` уже предусматривает `openBaoPath`. - Wizard выбора credential-типа (single vs split) — flag-driven + дефолт single PAT; визард по operator feedback. - Short-lived-token registry + refresher / kubelet credential provider — не нужно для classic-PAT GHCR; managed-era / cloud-registry concern. - GitHub App credential path — managed-era refinement (git-половина). - Backstage `SourceCredential` view — Phase 3 Backstage plugin. **Зависит от:** 1.79a (repo/app subcommands, `find_apps_matching_prefix`); **2.11** (SealedSecrets controller + kubeseal-in-CLI slice — **cross-phase, см. `speedrun-plan.md` §4.2 SR ordering**); 1.72–1.78 (MigrationPlan CRD — для destructive-gating подчасти). **Размер:** L --- ### 1.80 `apprafter platform fork` GitHub API automation > ⏸️ **DEFERRED (2026-06-01)** — power-user one-command fork; **не на managed-launch критическом пути** (no SR bucket marker). Явно отложен при закрытии M1.5 (решение пользователя). Не блокирует M1.5: `e2e/fork.sh` дропнут, docs fork = 1 строка-ссылка сюда. Подтянуть post-launch / по запросу power-users. **Source:** ADR 0028. **Цель:** one-command fork bootstrap для power users. **Поставка:** - [ ] `apprafter platform fork --to [--private]`: - Validates GitHub PAT exists (env or target credentials store). - Fork `github.com/AppRafter/apprafter` to user's GitHub account/org via API. - Add `.github/workflows/platform-stack-publish.yml` to the fork (copied from upstream — это same workflow что был залит в 1.68, отображённый для fork-specific OCI namespace). - Trigger initial publish (push tag → CI builds → OCI publishes). - Patch local PlatformStack CR: `spec.source.repoURL = `, keep `spec.source.upstream` pointing to AppRafter upstream for tracking. - [ ] Documentation в `docs/operator-guide/fork.md`: when to fork, how to maintain, sync from upstream procedure. **Acceptance:** - `apprafter platform fork --to ghcr.io/myorg --private` on test account → fork created, workflow added, initial OCI publish ends green, local cluster's PlatformStack updated to pull from `ghcr.io/myorg`. - Edit CUE in fork → tag → next bootstrap or upgrade pulls from fork. - Upstream tracking: PlatformStack.status.availableVersion still reflects AppRafter upstream releases. **Зависит от:** 1.68 (workflow template), 1.79 (CLI infra) **Размер:** M --- ### 1.81 e2e tests update **Source:** ADR 0025, 0026, 0027, 0028, 0029. **Цель:** end-to-end coverage всех new flows. > ✅ **CLOSED (2026-06-02):** k3d e2e-гейт зелёный (run `26815926016`, `gitops-walk.sh` 3m16s) → spec.md §6 M1.5 box флипнут + Revision 9→10. Деталь раунда ниже. **SCOPED CLOSE (2026-06-01, ревизия 2026-06-02):** локальный кластер проекта — **k3d** (не kind, формулировка ниже устарела). Сэндбокс ассистента **не запускает k3d** (Podman без `/var/run/docker.sock`) → k3d-скрипты написаны + `shellcheck`-clean + заведены в CI, валидатор — CI (аналог ручного walk). Общий `e2e/lib.sh` harness извлечён. **2026-06-02 (CI-разбор):** `migration-platform.sh` **снят с k3d-гейта** (Option C) — фундаментально несовместим со средой: PlatformController-OCI-клиент **HTTPS-only** (`oci-distribution ClientConfig::default()`), а локальный реестр k3d — **plain-HTTP**, поэтому контроллер не может стянуть compat-doc фикстур in-cluster → классификация перехода падает, план не создаётся даже на CR-уровне. Net: k3d-гейт = `gitops-walk.sh`; `mvp.sh` = Hetzner-nightly; миграционный гейт покрыт unit+integration тестами оператора + (follow-up) real-infra walk на nightly Hetzner. > > **Поставка:** - [x] `e2e/mvp.sh` rewritten — 9-step → 3-step (init → bootstrap-all → Application smoke); **+ассерт что platform-компоненты Argo-CD-managed** (не CLI-applied). Общий `e2e/lib.sh`. Hetzner-nightly. (commit `bfcd95d`) - [x] `e2e/gitops-walk.sh` — k3d: fixture app repo (local git daemon) → `app add` → CMP рендерит Application CR → Argo синкает → operator reconciles Deployment → push `Application.cue` change → сходится. shellcheck-clean; CI-validated. (commit `a7a4051`) - [~] `e2e/migration-app.sh` — **DEFER → Phase 2**: тестит `needs.pg` end-to-end provision (ResourceClaim + ServiceProvider + CloudNativePG); `needs` **явно удалён** из v1alpha1 (`schemas/v1alpha1/application.cue:18`), реализуем только после 2.2/2.3. - [~] `e2e/migration-platform.sh` — **DROP с k3d-гейта (2026-06-02, Option C)**: скрипт удалён. CI-разбор вскрыл, что walk нереализуем на k3d на нескольких уровнях, и все блокеры упираются в **неотгруженную/отложенную** инфру, а не в баг гейта (1.78 корректен, покрыт unit+integration): (1) PlatformController-OCI **HTTPS-only** vs plain-HTTP реестр k3d → compat-doc фикстур не тянется; (2) `PlatformController` пробрасывает родителю только `targetRevision`+`helm.values`, **не `repoURL`** → реальный деплой редиректнутого реестра = fork-история **1.80 (deferred)**; (3) попытка пропатчить `targetRevision` на отсутствующую в ghcr версию вешает in-flight гейт; (4) исходный скрипт был написан против неверной модели триггера (доступность-сама-создаёт-план вместо pin→breaking). Корректный blueprint будущего real-infra теста (pin-driven, nudge PlatformStack после `approve`, HTTPS-реестр) — в memory `project_m1_5_close`. **Follow-up'ы:** (a) env-gated insecure/HTTP OCI в контроллере (dev-mode/1.9 территория); (b) `apprafter migration approve` мог бы пинать PlatformStack — сейчас bump доезжает лишь на следующем reconcile (до `checkInterval` ≤6h), т.к. PlatformController не watch'ит MigrationPlan. - [~] `e2e/fork.sh` — **DROP**: item 1.80 (`platform fork`) сейчас не делаем. - [x] All scripts callable from CI; budget < 30 min per script на **k3d** cluster — `Justfile` (`e2e`/`e2e-gitops`) + CI `e2e-k3d.yml` (PR-triggered); `mvp.sh` остаётся nightly. (commit `6921670`; dist-render step + `e2e-migration-platform` recipe убраны 2026-06-02 вместе с descope) **Acceptance:** - [x] **(CI-gate GREEN 2026-06-02, run `26815926016`)** `just e2e` (= `gitops-walk.sh`) ran green on k3d in CI in 3m16s — bootstrap (Cilium→Argo→platform-stack 0.1.52, PlatformStack/default tier=1) + the full loop (`app add` → CMP render → Argo Synced/Healthy → operator Deployment Available → replicas 1→2 propagates). `mvp.sh` is the nightly real-Hetzner complement. **This flipped the spec.md §6 M1.5 box + Revision 9→10.** - Major M1.5 code paths exercised: cluster-bootstrap + Application reconcile (mvp), CMP→Argo→operator loop (gitops-walk). PlatformStack pin→breaking→MigrationPlan→approve/reject gate covered by **operator unit + integration tests** (k3d walk descoped — see migration-platform.sh note above). `needs.pg`/migration-app path → Phase 2; fork path → 1.80 (deferred); real-infra migration walk → nightly-Hetzner follow-up. **Зависит от:** all 1.66–1.80 (1.80 deferred — `fork.sh` dropped accordingly) **Размер:** M --- ### 1.82 Docs update **Source:** ADR 0025, 0026, 0027, 0028, 0029. **Цель:** rewrite outdated quickstart, add new operator/dev guides. **Поставка:** > ✅ **DONE (2026-06-01, commit `a7efe69`)** — все доки написаны, `mkdocs build --strict` чист (0 warnings/errors), `cyrillic`-хук clean. Fork = одна строка-defer (1.80). Доки реально валидируемы (в отличие от e2e). - [x] `docs/operator-guide/quickstart.md` rewritten: 3-step (install → `target add`/`init` → `bootstrap-all`), Argo-CD-managed platform на first read, `apprafter open argocd`, **CX22 → CPX22**, smoke через `Application` CRD. - [x] `docs/operator-guide/platform-management.md` (new): PlatformStack lifecycle, каналы (stable/beta/edge), upgrade/freeze/rescue. **Fork = 1 строка** (power-user, 1.80 not yet shipped) — не отдельная секция (scoped). - [x] `docs/operator-guide/migration-plans.md` (new): destructive change; approve/reject by scope (application = approve-only + revert commit per ADR 0027; platform = approve/reject); surfaces (CLI сейчас; Backstage/Argo UI — позже). - [x] `docs/dev-guide/application-cue.md` (new): `apprafter/Application.cue`, CMP render + troubleshooting, multi-env (`spec.environments` + `APPRAFTER_ENV`). - [x] `docs/operator-guide/gitops-walk.md` updated: Application-CRD end-to-end (не raw Deployment+Service); + `mkdocs.yml` nav. - [x] Update root `README.md` reference links + dev-quickstart CX22→CPX22 fix. **Acceptance:** - New user reading quickstart end-to-end can get to running app in ~30 min. - Docs explain Argo CD's role clearly without contradictions. - Mental model "platform reconciles itself" передаётся on first reading. **Зависит от:** 1.81 **Размер:** S --- ### 1.83a Chart: gateway component (host-network) + gateway-api-crds + per-domain listeners + redirect > 🏁 SR: A · order 3.5 — public ingress MVP (completes T1 demo); minimal slice — full ingress остаётся order 5 (4.1/4.1a/4.4a) > 🔁 **ПИВОТ при реализации (2026-06-11): host-network, НЕ LB-IPAM+L2.** Cilium L2-announcement — bare-metal-only (ARP на общем L2-сегменте, IPv6/NDP не поддерживает); на Hetzner Cloud публичный IP узла маршрутизируется SDN провайдера, не через ARP → LB-IPAM/L2 не назначат и не анонсируют адрес. Правильный механизм для single-node-cloud (у узла уже есть публичный IP) — Gateway API **host-network mode**: Envoy биндит host-netns порты 80/443 прямо на узле, `node-IP:443` → Gateway, БЕЗ LoadBalancer-Service / LB-IPAM / L2. Релиз **platform-stack 0.2.25** (chart-only, БЕЗ CLI — cilium loader-values байт-идентичны). Build отгружен на master; **1.83a ЗАКРЫТ 2026-06-13** — чистый T8 live-Hetzner-ретест зелёный (F1–F4, мульти-домен, teardown, без ручных рестартов) после фиксов 0.2.27 (F1/F2-v1) → 0.2.32 (F2-rollOutPods весь cilium-стек) + cli v0.2.14 (F3 ignoreDifferences) + F4-резолв через F3-Gateway. **Source:** инфра-долг (servicelb выключен с 1.3); precursor для 4.1 platform Gateway. **Цель:** платформенный Gateway на одном узле публично обслуживает домены (host-network: Envoy слушает 80/443 на публичном IP узла). Для каждой entry в `gateway.allowedDomains[]` — пара HTTPS-listener'ов (apex + wildcard) со static cert-ref на per-entry imported-cert Secret + один catch-all HTTP listener + platform-wide redirect HTTPRoute (HTTP→HTTPS 301). Пустой массив ⇒ ноль gateway-ресурсов (zero-impact на existing solo). **Поставка (отгружено в 0.2.25, host-network):** - [x] `gateway.allowedDomains` values-схема (`#AllowedDomainEntry` + `#GatewayValues` в `platform.cue`; default пусто). Поля entry: `domain`, `certMode:"imported"`, `importedCertRef`, `addedAt`, `addedBy`; + `nodePublicIP?`/`nodePublicIPv6?` (для DNS / 1.83f; в host-network чартом не используются). - [x] Cilium КОМПОНЕНТ (Argo-managed, НЕ loader) включает Gateway API **host-network mode**: `gatewayAPI.hostNetwork.enabled` + envoy `NET_BIND_SERVICE` caps (standalone-envoy, cilium 1.16.5). Loader-values **байт-идентичны** (no CLI change, no CLI bump) — паттерн `_loaderValues.cilium.values & {extras}` как у argocd-компонента. Прежние `l2announcements`/`externalIPs` — дропнуты. - [x] `gateway-api-crds` Argo-компонент (sync-wave **−25**, до cilium −20) — ставит upstream Gateway API **v1.2.1** CRD (git-source `kubernetes-sigs/gateway-api`, path `config/crd/standard`; прецедент network-policies). Доставка через Argo, НЕ через bootstrap (bootstrap минимален). - [x] `gateway.yaml` render-шаблон (umbrella-emitted в `render_tool.cue`, guarded `{{- if .Values.gateway.allowedDomains }}`): `Gateway` `platform` в `apprafter-system` (gatewayClassName cilium, label `apprafter.io/platform-gateway: "true"` — adoption-shim для 4.1; per-entry apex+wildcard HTTPS-listener'ы, имена dot-free через `replace "." "-"` + http catch-all) + `platform-http-redirect` HTTPRoute (301). **БЕЗ** LoadBalancer-Service / `CiliumLoadBalancerIPPool` / `CiliumL2AnnouncementPolicy` (дропнуты — host-network их не требует). - [x] Регресс-гард `scripts/check-gateway-render.sh` + врезка в CI (`platform-stack-check`): empty→ноль gateway-ресурсов; populated→1 Gateway + 4 https-listener'а (apex+wild ×2) + 1 http + 1 redirect-301, без IPPool/L2; dot-free имена. - [x] kind apiserver-валидация (структурная приёмка Gateway+HTTPRoute против v1.2.1 CRD; PROGRAMMED не валидируется — нет cilium-контроллера в kind) + релиз platform-stack **0.2.25** (`currentVersion`/compat `change: requires-restart`; operator/cli не бампались). **T8 walk-fixes (live-Hetzner 2026-06-12 — known gotchas cilium host-network Gateway on GitOps):** - [x] **F1 (BLOCKER, 0.2.27):** `gateway-api-crds` → **experimental** канал (`config/crd/experimental`), не standard — cilium 1.16.5 gateway-контроллер на старте требует `tlsroutes.gateway.networking.k8s.io` (v1alpha2, только в experimental); без него контроллер падает на required-resources-check → **Gateway никогда не Programs** (NB: `GatewayClass.Accepted` на cilium 1.16.5 остаётся `Unknown` даже при здоровом Programmed Gateway — несущий сигнал = `Gateway.Programmed`, НЕ GatewayClass.Accepted; мой прежний runbook-чек на GatewayClass был неверным). + регресс-гард в `check-gateway-render.sh`. - [x] **Gateway-walk регресс-гард (sandbox-run — дыра, что kind не закрыл):** новый `e2e/gateway-walk.sh` — реальный Cilium 1.16.5 + gateway-контроллер под `sandbox-run` (rootful microVM, memlock unlimited; в kind под rootless podman cilium-gateway не живёт) → experimental CRD + наш отрендеренный `gateway.yaml` → assert `Gateway.Programmed=True` + listeners. **Прогнан ЗЕЛЁНЫМ:** positive (experimental → Programmed) + negative (`APPRAFTER_GW_WALK_NEGATIVE=1`: standard → Gateway никогда не Programs — F1-симптом воспроизведён). + nightly workflow. Закрывает test-gap: контроллерный уровень F1 был sandbox-валидируем (а я пунтанул всё в Hetzner). - [x] **F2 (operability — РАСШИРЕН в Run 2, 0.2.27→0.2.32):** cilium читает config-ФЛАГИ только на старте → включение gateway-api/host-network на ЖИВОМ кластере требует рестарта ВСЕГО стека: **agent (`ds/cilium`** — отдаёт CiliumEnvoyConfig в Envoy по xDS), **operator** (Gateway→CEC), **standalone envoy (`ds/cilium-envoy`** — биндит host-netns 80/443). Run-2 walk поймал: Gateway `Programmed=True`, но Envoy не биндит 80/443, т.к. agent на старом конфиге не пушит CEC-listener → только ручной `rollout restart` всех трёх анблокировал. Прежний 0.2.27-фикс (`operator.podAnnotations.cilium-config-rev`) катил ТОЛЬКО оператора. **Заменён на штатные cilium-values `rollOutCiliumPods`/`operator.rollOutPods`/`envoy.rollOutPods: true`** (config-checksum на pod-template → авто-ролл каждого пода при изменении cilium-config) — 0.2.32, chart-only (loader байт-идентичен, no operator/CLI). Fresh-инсталлы не затронуты (upgrade-корректность). - [x] **F3 (cosmetic, CLI v0.2.14):** `ignoreDifferences` на root `platform` Application — иначе Gateway+HTTPRoute вечно OutOfSync в Argo (selfHeal зря churn'ит). **Поля по ЖИВОМУ Argo DIFF (T8 Run 2 — apiserver-дефолтинг, НЕ финалайзеры):** Gateway `.spec.listeners[].tls.certificateRefs[].group` (apiserver дозаполняет пустой core-group `''`); HTTPRoute `.spec.parentRefs[].group` (`gateway.networking.k8s.io`) + `.spec.parentRefs[].kind` (`Gateway`) + `.spec.rules[].matches` (apiserver дефолтит match-all `PathPrefix /`, наш redirect-rule без matches). В `render_root_application` (новые bootstrap'ы) + регресс-тест `render_root_application_ignores_gateway_api_defaulting`; existing-кластер — одноразовый `kubectl patch application platform ... spec.ignoreDifferences` (поле оператор НЕ владеет → переживает его SSA-патчи). - [x] **T8 — live Hetzner host-network smoke (приёмка) — ✅ ВЗЯТ (чистый ретест 2026-06-13, F1–F4 все зелёные вживую, БЕЗ ручных рестартов):** Gateway `Programmed=True` за 1s, 3 listener'а Accepted/Programmed/ResolvedRefs, `curl -I http://demo.apprafter.dev` → **301** → https, :443 TLS-терминируется (404 = нет app-route); **мульти-домен** (2-я entry demo2 → +2 listener'а apex+wildcard, curl 301); **teardown** (`allowedDomains:[]`) → Gateway+HTTPRoute сами `NotFound`, root App `Synced`; F2-rollOutPods отработал (Programmed без ручного restart'а cilium-стека), F3-ignoreDifferences → root Synced, F4 → проекция+prune сами. GatewayClass `Accepted=Unknown` — informational cilium-quirk, не гейтили. Gateway `Programmed=True`, Envoy биндит ОБА порта 80/443, `curl -I http://` → 301. F1+F2 разблокируют Programmed (0.2.27). Острые углы: cilium [#42786](https://github.com/cilium/cilium/issues/42786) (PROGRAMMED:False)=F2; [#39422](https://github.com/cilium/cilium/issues/39422) (80+443) — по `ss -tlnp`. Домен/серт вручную (1.83e/1.83f дальше). **Runbook (исправлен R1-R4):** `NODE_IP` из `InternalIP`+`awk '{print $1}'` (Hetzner без CCM не вешает ExternalIP; InternalIP отдаёт v4+v6); проверять cilium-config, НЕ Argo-`application` (CRD-коллизия с `applications.apprafter.io`); openssl через `nix run nixpkgs#openssl req --`; без `yq`. Полные команды — в отчёте/ответе ассистента 2026-06-12. **Acceptance (= T8):** - Создать `kubernetes.io/tls` Secret (SAN на apex+wildcard) + выставить `PlatformStack.spec.values.gateway.allowedDomains: [{domain, certMode:"imported", importedCertRef, addedAt, addedBy}]` ⇒ `kubectl get gateway platform -n apprafter-system` → `Programmed: True`, listener'ы `https-apex-` + `https-wild-` + `http`. - Вторая entry ⇒ ещё два HTTPS-listener'а после reconcile, каждый на свой Secret. - `curl -I http:///` → `301` `Location: https:///`; `curl -Ik https:///` достаёт Gateway. - Пустой массив ⇒ нет gateway-ресурсов; existing solo-кластера не ломаются (cilium переконфигурируется на host-network через Argo на апдейте — `change: requires-restart`). **Зависит от:** 1.4 ✅, 1.71 ✅. **Размер:** M+ (build отгружен; остаётся T8 live-Hetzner-walk) --- ### 1.83b Schema: `expose.hostname` + `expose.tls` + allowedDomains admission > 🏁 SR: A · order 3.5 — public ingress MVP (completes T1 demo); minimal slice — full ingress остаётся order 5 (4.1/4.1a/4.4a) > ✅ **CLOSED 2026-06-13** (delivered as ONE vertical, merging 1.83b schema/webhook + 1.83c operator HTTPRoute). Release: operator v0.2.30 / platform-stack 0.2.33 / cli v0.2.15. kind+Cilium live walk GREEN (e2e/gateway-walk.sh). **Deviations from the original 1.83b/1.83c wording:** (1) `expose.public` (bool) DROPPED — `network: "public"` is the single visibility trigger (the bool was never consumed). (2) `expose.tls` KEPT as the minimal `bool | *true` (full `#TlsOptions` deferred to 4.1b, NOT cancelled); `tls:false`+public is webhook-rejected for now. (3) The hostname↔allowedDomains zone check is a SOFT operator `PublicRouteReady` condition (route still emitted; adding the zone later attaches it), NOT a hard webhook reject — the webhook only checks DNS-1123-subdomain shape. (4) The landing-manifest migration to `expose.hostname` (1.83b "Поставка" last bullet) is a SEPARATE landing-repo change, NOT done here. **Source:** срез 4.1a schema extension + 4.1b `allowedDomains` admission rule. **Цель:** ввести поля `hostname` и `tls` в Application; admission webhook валидирует hostname против `PlatformStack.spec.values.gateway.allowedDomains`. Оба rules signature-stable с 4.1b — миграция == webhook читает `ExternalSurface.spec.allowedDomains` вместо PlatformStack. **Поставка:** - [x] `schemas/v1alpha1/application.cue` — расширить `#Expose`: ```cue #Expose: { port: int public: bool | *false network: "public" | "internal" | "vpn" | *"internal" hostname: string | *"" // required when public: true (validated в admission) tls: bool | *true // default on для public; full #TlsOptions в 4.1b } ``` - [x] OpenAPI v3 CRD пересгенерирована (или hand-rolled mirror) + apply через chart upgrade. - [x] Admission webhook (existing `apprafter-admission-webhook`): - Rule 1: `public: true ⇒ hostname != ""` с error «expose.public: true requires expose.hostname (defaultDomain auto-generation lands в 4.1b)». - Rule 2: `public: true` ⇒ `hostname` matches минимум одной entry в `PlatformStack.spec.values.gateway.allowedDomains[]`: - Exact apex match: `hostname == entry.domain`. - Single-label wildcard match: `hostname == "