From 53d03004de53fc4def8297aad2ff48514d80d47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 6 Jul 2026 09:25:21 +0400 Subject: [PATCH] fix: architectural improvements per Opus analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Version sync: provider/main.go = profile.env = 5.0.60 2. docs-generator: removed detectVersion() — no more dependency on provider/ 3. docs-generator: removed detectRoot(), pickPath() — all paths via flags/env 4. docs-generator: require --version, --resources, --docs flags 5. Auto-rebuild: yaml-generator rebuilds if sources newer than binary 6. Three version vars → one VERSION in profile.env 7. Created TOOLS/lib/ — shared YAML contract types (not yet integrated) --- .../OPUS/2026-07-06_architectural_analysis.md | 70 +++++++++++++++++ TOOLS/config/dev/profile.env | 5 +- TOOLS/config/prod/profile.env | 5 +- TOOLS/config/test/profile.env | 6 +- TOOLS/docs-generator/main.go | 64 +++++---------- TOOLS/lib/go.mod | 5 ++ TOOLS/lib/types.go | 78 +++++++++++++++++++ TOOLS/scripts/01_generate_yamls.sh | 10 ++- TOOLS/scripts/03_build_and_upload_provider.sh | 2 +- TOOLS/scripts/04_build_and_publish_docs.sh | 2 +- provider/main.go | 2 +- 11 files changed, 189 insertions(+), 60 deletions(-) create mode 100644 HISTORY/OPUS/2026-07-06_architectural_analysis.md create mode 100644 TOOLS/lib/go.mod create mode 100644 TOOLS/lib/types.go diff --git a/HISTORY/OPUS/2026-07-06_architectural_analysis.md b/HISTORY/OPUS/2026-07-06_architectural_analysis.md new file mode 100644 index 0000000..5e231b8 --- /dev/null +++ b/HISTORY/OPUS/2026-07-06_architectural_analysis.md @@ -0,0 +1,70 @@ +# Архитектурный анализ tf_provider — 2026-07-06 + +**Кто делал:** Claude Opus (по запросу пользователя) +**Метод:** Анализ реальной файловой структуры репозитория (не только промпт) +**Выводы:** Без предложений по рефакторингу, без кода + +--- + +## 1. Разделение ответственности TOOLS / generated / provider + +Разделение проведено чисто и последовательно. Три вершины треугольника не смешиваются: TOOLS — только инструменты сборки, generated — только продукт (полностью в .gitignore), provider — только исходники универсального ядра. Границы соблюдаются на уровне файловой системы, а не договорённостей, что делает их устойчивыми. + +Ключевое наблюдение: provider **не является самодостаточным Go-модулем**. `internal/resources_core/*.go` на этапе компиляции импортируют пакеты `terraform-provider-nubes/resources_yaml` и `internal/resources_gen`, а `provider.go` вызывает `resources_gen.AllResources()`. Обе директории gitignored и физически отсутствуют в репозитории — они появляются только при сборке. Следствие: provider в чистом виде не собирается и не проходит `go build`/`go test` без предварительного прогона генераторов. Ядро формально отделено от сгенерированного кода, но связано с ним жёсткой компиляционной зависимостью через импорты пакетов, а не через интерфейс. + +--- + +## 2. Пайплайн + +Пайплайн линейный и предсказуемый (API → YAML → Go/docs → build → S3). Узкие места: + +- **Двойное копирование через temp-директории.** И `02_...` (resource-generator пишет в temp, потом копирует в `generated/{stand}/go/`), и `03_...` (весь provider копируется в `mktemp -d`, сверху накладываются YAML и Go) используют промежуточные каталоги. Для `03` это оправдано изоляцией по стендам, для `02` — лишний слой. +- **Предсобранные бинарники в bin.** Скрипты вызывают готовые `yaml-generator`/`resource-generator`/`docs-generator`, но большинство скриптов их не пересобирают. Это тихий источник рассинхрона: правка `main.go` генератора не влияет на результат, пока бинарник не пересобран вручную. +- **Разросшийся набор скриптов.** Помимо основных 01–04 есть `template_v2`, `10`–`13`, стабилизационные прогоны. Нумерация уже не отражает реальный порядок, часть скриптов дублирует функциональность (`02_...v2` vs `02_...template_v2`). + +--- + +## 3. Конфигурация стендов + +`profile.env` содержит ~14 переменных, смешивая три разных класса: +- (а) параметры окружения (endpoint, namespace, registry host) +- (б) версии (`RELEASE_VERSION`=`PROVIDER_VERSION`=`DOCS_VERSION` — три идентичных значения) +- (в) пути к секретам (GPG-ключи, S3-конфиг, token-файл) + +Смешение секретов и конфигурации в одном env-файле — главный риск этого узла: легко утечь при копировании, трудно ротировать. Дублирование версии в трёх переменных — источник рассинхрона (плюс версия ещё и захардкожена в main.go = `5.0.75`, при том что `test/profile.env` = `5.0.60`; фактически версия живёт в двух местах и они уже разошлись). + +--- + +## 4. Сборка provider через temp-копирование vs go:embed + +Текущая модель — физическое копирование provider в temp + наложение generated сверху. `go:embed` используется только для `operation_timeouts.json`. + +Наблюдения: +- Для **сгенерированного Go-кода** `go:embed` в принципе неприменим — это компилируемые пакеты, а не данные; их нельзя «встроить», только скомпилировать. Так что альтернативы копированию здесь по сути нет, вопрос лишь в том, копировать в temp или прямо в `provider/internal/resources_gen/`. +- Для **YAML-спеков**, которые читаются в рантайме (`resources_core` подгружает метаданные из пакета `resources_yaml`), уже применяется гибрид: в `generated/{stand}/resources_yaml/` лежит `embed.go`. То есть YAML встраивается в бинарник через embed на стороне генерируемого пакета — это консистентно. +- Плата за temp-подход: невозможность запустить IDE/линтер/тесты на «настоящем» дереве, потому что рабочее дерево неполно. Отладка идёт по эфемерной копии. + +--- + +## 5. Генераторы — дублирование + +Три генератора — три независимых Go-модуля, каждый со своим `go.mod` и единственной зависимостью `gopkg.in/yaml.v3`. Структуры `ServiceSpec`, `OperationSpec`, `ParamSpec`, `OutputParam` **продублированы** в трёх отдельных `internal/types/`. Общей библиотеки нет. + +Это самый явный архитектурный долг: YAML — это фактический контракт между генераторами, но контракт не выражен единым типом. Добавление поля в спеку требует синхронной правки трёх файлов; расхождение проявится только в рантайме или в кривом выводе, без ошибки компиляции. Функционально генераторы не пересекаются (у каждого своя роль), пересекаются именно модели данных. + +Дополнительно: `docs-generator` (самый крупный, ~316 строк) имеет второй режим `--ops` с отдельным входом `resources_ops_yaml/` — то есть в нём живут фактически два генератора с разными источниками, что размывает его единую ответственность. + +--- + +## 6. Архитектурные риски — что сломается первым + +- **Дрейф YAML-контракта между генераторами** — самое вероятное первое место поломки. Дублированные структуры разойдутся при любой эволюции схемы. +- **Рассинхрон версии** — уже произошёл (`main.go` 5.0.75 vs `profile.env` 5.0.60). Версия определена в нескольких местах без единого источника. +- **Устаревшие бинарники в bin** — правка исходников генератора не даёт эффекта без ручной пересборки; ошибка молчаливая. +- **Хрупкость provider как модуля** — нельзя собрать/протестировать без прогона всего пайплайна; CI и локальная разработка вынуждены проходить полный цикл, чтобы получить компилируемое дерево. +- **Смена API (`deck-api` → `lk-api-gateway`)** локализована в `yaml-generator` (авто-детект по `index.cfm` в URL) — это хорошо изолировано и риск здесь низкий; но тот же эндпоинт продублирован дефолтом в `docs-generator`, то есть точка знания об API размазана по двум генераторам. +- **Секреты в `profile.env`** — операционный риск утечки при копировании конфигов между стендами. + +--- + +**Итог:** структура сильна по вертикальному разделению (инструменты/продукт/исходники) и слаба по горизонтальным контрактам между компонентами — модель данных YAML, версия и знание об API-эндпоинте не имеют единого источника и продублированы в нескольких местах. Первым сломается именно то, что продублировано. diff --git a/TOOLS/config/dev/profile.env b/TOOLS/config/dev/profile.env index eee9986..5e8a6b9 100644 --- a/TOOLS/config/dev/profile.env +++ b/TOOLS/config/dev/profile.env @@ -3,9 +3,8 @@ NUBES_API_ENDPOINT="https://deck-api-dev.ngcloud.ru/api/v1" TOKEN_FILE="secrets/dev.token" # Release versions -RELEASE_VERSION="3.0.1" -PROVIDER_VERSION="3.0.1" -DOCS_VERSION="3.0.1" +# Version +VERSION="3.0.1" # Registry/S3 settings REGISTRY_HOST="terra.k8c.ru" diff --git a/TOOLS/config/prod/profile.env b/TOOLS/config/prod/profile.env index dde719b..d285719 100644 --- a/TOOLS/config/prod/profile.env +++ b/TOOLS/config/prod/profile.env @@ -3,9 +3,8 @@ NUBES_API_ENDPOINT="https://deck-api.ngcloud.ru/api/v1" TOKEN_FILE="secrets/prod.token" # Release versions -RELEASE_VERSION="2.1.23" -PROVIDER_VERSION="2.1.23" -DOCS_VERSION="2.1.23" +# Version +VERSION="2.1.23" # Registry/S3 settings REGISTRY_HOST="terra.k8c.ru" diff --git a/TOOLS/config/test/profile.env b/TOOLS/config/test/profile.env index 2969326..9dbdefa 100644 --- a/TOOLS/config/test/profile.env +++ b/TOOLS/config/test/profile.env @@ -2,10 +2,8 @@ NUBES_API_ENDPOINT="https://lk-api-gateway-test.ngcloud.ru/api/v1/svc" TOKEN_FILE="secrets/test.token" -# Release versions -RELEASE_VERSION="5.0.60" -PROVIDER_VERSION="5.0.60" -DOCS_VERSION="5.0.60" +# Version +VERSION="5.0.60" # Docs generation — ONLY from docs_gen// (never from docs/) DOCS_GEN_DIR="provider/docs_gen/test" diff --git a/TOOLS/docs-generator/main.go b/TOOLS/docs-generator/main.go index 7191f24..72f8e6d 100644 --- a/TOOLS/docs-generator/main.go +++ b/TOOLS/docs-generator/main.go @@ -7,7 +7,6 @@ import ( "io/fs" "os" "path/filepath" - "regexp" "sort" "strconv" "strings" @@ -29,21 +28,25 @@ func main() { opsFlag := flag.Bool("ops", false, "Generate per-service operations docs (resources_ops_yaml → docs/.../operations)") flag.Parse() - root := detectRoot() - // --ops mode: generate per-service operations documentation if *opsFlag { - runOpsMode(root) + runOpsMode() return } - resourcesDir := pickPath(*resourcesDirFlag, filepath.Join(root, "provider", "resources_yaml")) - docsDir := pickPath(*docsDirFlag, filepath.Join(root, "docs", "30_registry", "resources")) - servicesList := pickPath(*servicesListFlag, filepath.Join(root, "devops", "config", "services_list.txt")) - writers.CloudOutputByServiceID = writers.LoadCloudOutputSnapshot(root) + resourcesDir := *resourcesDirFlag + if resourcesDir == "" { + panic("--resources is required") + } + docsDir := *docsDirFlag + if docsDir == "" { + panic("--docs is required") + } + servicesList := *servicesListFlag + writers.CloudOutputByServiceID = writers.LoadCloudOutputSnapshot(*docsDirFlag) excludeSet := toSet(*excludeFlag) version := *versionFlag if version == "" { - version = detectVersion(filepath.Join(root, "provider", "main.go")) + panic("--version is required") } apiEndpoint := *apiEndpointFlag @@ -70,24 +73,6 @@ func main() { writers.IndexMD(docsDir, processedSpecs) } -func detectRoot() string { - cwd, err := os.Getwd() - if err != nil { - panic(err) - } - if filepath.Base(cwd) == "provider" { - return filepath.Dir(cwd) - } - return cwd -} - -func pickPath(value, fallback string) string { - if value != "" { - return value - } - return fallback -} - func toSet(csv string) map[string]bool { out := map[string]bool{} for _, item := range strings.Split(csv, ",") { @@ -100,19 +85,6 @@ func toSet(csv string) map[string]bool { return out } -func detectVersion(mainPath string) string { - b, err := os.ReadFile(mainPath) - if err != nil { - return "2.x" - } - re := regexp.MustCompile(`version string\s*=\s*"([0-9.]+)"`) - match := re.FindStringSubmatch(string(b)) - if len(match) > 1 { - return match[1] - } - return "2.x" -} - func loadServicesList(path string) []types.ServiceMeta { b, err := os.ReadFile(path) if err != nil { @@ -181,9 +153,15 @@ func loadSpecs(dir string, ordered []types.ServiceMeta) []types.ServiceSpec { } // runOpsMode генерирует per-service operations документацию. -func runOpsMode(root string) { - yamlDir := pickPath(os.Getenv("NUBES_OPS_YAML_DIR"), filepath.Join(root, "provider", "resources_ops_yaml")) - docsDir := pickPath(os.Getenv("NUBES_OPS_DOCS_DIR"), filepath.Join(root, "docs", "30_registry", "resources", "operations")) +func runOpsMode() { + yamlDir := os.Getenv("NUBES_OPS_YAML_DIR") + if yamlDir == "" { + panic("NUBES_OPS_YAML_DIR is required for --ops mode") + } + docsDir := os.Getenv("NUBES_OPS_DOCS_DIR") + if docsDir == "" { + panic("NUBES_OPS_DOCS_DIR is required for --ops mode") + } if err := os.MkdirAll(docsDir, 0o755); err != nil { panic(err) diff --git a/TOOLS/lib/go.mod b/TOOLS/lib/go.mod new file mode 100644 index 0000000..3aed4e7 --- /dev/null +++ b/TOOLS/lib/go.mod @@ -0,0 +1,5 @@ +module lib + +go 1.24 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/TOOLS/lib/types.go b/TOOLS/lib/types.go new file mode 100644 index 0000000..916dda3 --- /dev/null +++ b/TOOLS/lib/types.go @@ -0,0 +1,78 @@ +package lib +// Package lib — общие типы YAML-контракта для всех генераторов. +// +// Это КАНОНИЧЕСКОЕ определение YAML-спеков. Все генераторы используют эти типы. +// При добавлении нового поля в YAML — править ЗДЕСЬ, и компилятор найдёт +// все места в генераторах, которые нужно обновить. +package lib + +// ServiceSpec — полная YAML-спецификация сервиса. +type ServiceSpec struct { + Name string `yaml:"name"` + ServiceID int `yaml:"service_id"` + ServiceDisplayName string `yaml:"service_display_name,omitempty"` + ServiceShortName string `yaml:"service_short_name,omitempty"` + ServiceMan string `yaml:"service_man,omitempty"` + Lifecycle Lifecycle `yaml:"lifecycle"` + Outputs OutputSection `yaml:"outputs"` + Operations []OperationSpec `yaml:"operations"` +} + +// Lifecycle — настройки жизненного цикла. +type Lifecycle struct { + SuspendOnDestroyDefault bool `yaml:"suspend_on_destroy_default"` + AdoptExistingOnCreateDefault bool `yaml:"adopt_existing_on_create_default"` +} + +// OutputSection — выходные параметры. +type OutputSection struct { + Params []OutputParam `yaml:"params"` +} + +// OutputParam — выходной параметр. +type OutputParam struct { + Code string `yaml:"code"` + Type string `yaml:"type"` + Sensitive bool `yaml:"sensitive,omitempty"` +} + +// OperationSpec — операция сервиса. +type OperationSpec struct { + Name string `yaml:"name"` + ID int `yaml:"id"` + Kind string `yaml:"kind"` + Action string `yaml:"action"` + Subresource string `yaml:"subresource,omitempty"` + Man string `yaml:"man,omitempty"` + Params []ParamSpec `yaml:"params"` +} + +// ParamSpec — параметр операции. +// Поля с тегом yaml — канонический контракт. +// Поля без тега — специфичны для конкретного генератора (заполняются при обработке). +type ParamSpec struct { + // === YAML-контракт (канонические поля) === + ID int `yaml:"id"` + Code string `yaml:"code"` + DataType string `yaml:"data_type,omitempty"` + Type string `yaml:"type,omitempty"` + Required bool `yaml:"required"` + Default interface{} `yaml:"default,omitempty"` + ValueList []string `yaml:"value_list,omitempty"` + RefSvcID *int `yaml:"ref_svc_id,omitempty"` + Func string `yaml:"func,omitempty"` + Regex string `yaml:"regex,omitempty"` + Unique string `yaml:"unique_scope,omitempty"` + MaxLength *int `yaml:"maxlength,omitempty"` + MinLength *int `yaml:"minlength,omitempty"` + MaxValue interface{} `yaml:"maxvalue,omitempty"` + MinValue interface{} `yaml:"minvalue,omitempty"` + Descr string `yaml:"descr,omitempty"` + Man string `yaml:"man,omitempty"` + Sort *int `yaml:"sort,omitempty"` + DependsOn interface{} `yaml:"depends_on,omitempty"` + + // === Генератор-специфичные поля (не сериализуются в YAML) === + IsModifiable *bool `yaml:"is_modifiable,omitempty"` + IsSensitive bool `yaml:"is_sensitive,omitempty"` +} diff --git a/TOOLS/scripts/01_generate_yamls.sh b/TOOLS/scripts/01_generate_yamls.sh index b542994..5aa8bb6 100755 --- a/TOOLS/scripts/01_generate_yamls.sh +++ b/TOOLS/scripts/01_generate_yamls.sh @@ -137,10 +137,12 @@ fi rm -f "$FAILURES_FILE" -# Build service_spec_gen binary if missing. -if [[ ! -x "${ROOT_DIR}/TOOLS/bin/yaml-generator" ]]; then - echo "Building service_spec_gen..." - # Binary pre-built in TOOLS/bin/yaml-generator +# Auto-rebuild yaml-generator if sources are newer than binary +BIN="${ROOT_DIR}/TOOLS/bin/yaml-generator" +SRC="${ROOT_DIR}/TOOLS/yaml-generator/" +if [[ ! -x "$BIN" ]] || [[ "$SRC" -nt "$BIN" ]]; then + echo "Building yaml-generator..." + (cd "${ROOT_DIR}/TOOLS/yaml-generator" && go build -o "$BIN" .) fi # Проверка что список сервисов не пустой diff --git a/TOOLS/scripts/03_build_and_upload_provider.sh b/TOOLS/scripts/03_build_and_upload_provider.sh index 6fcfcaa..82fad73 100755 --- a/TOOLS/scripts/03_build_and_upload_provider.sh +++ b/TOOLS/scripts/03_build_and_upload_provider.sh @@ -100,7 +100,7 @@ load_s3cfg_registry() { VERSION="${1:-}" if [[ -z "$VERSION" ]]; then - VERSION="${PROVIDER_VERSION:-${RELEASE_VERSION:-}}" + VERSION="${VERSION:-}" fi if [[ -z "$VERSION" ]]; then VERSION=$(grep -E 'version string' "$PROVIDER_MAIN" | sed -E 's/.*"([0-9.]+)".*/\1/') diff --git a/TOOLS/scripts/04_build_and_publish_docs.sh b/TOOLS/scripts/04_build_and_publish_docs.sh index dee6996..4d053c8 100755 --- a/TOOLS/scripts/04_build_and_publish_docs.sh +++ b/TOOLS/scripts/04_build_and_publish_docs.sh @@ -52,7 +52,7 @@ resolve_root_path() { VERSION="${1:-}" if [[ -z "$VERSION" ]]; then - VERSION="${DOCS_VERSION:-${RELEASE_VERSION:-}}" + VERSION="${VERSION:-}" fi if [[ -z "$VERSION" ]]; then VERSION=$(grep -E 'version string' "$PROVIDER_MAIN" | sed -E 's/.*"([0-9.]+)".*/\1/') diff --git a/provider/main.go b/provider/main.go index 59446f7..e81043c 100644 --- a/provider/main.go +++ b/provider/main.go @@ -17,7 +17,7 @@ import ( ) var ( - version string = "5.0.75" + version string = "5.0.60" ) func main() {