diff --git a/README.md b/README.md index 1839da5..b08b2d2 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ Each entry in `private_network_cidrs` is one network shared by *all* routers - e New Terraform variables: `router_count`, `private_network_cidrs`, `router_availability_zones` (see `terraform/variables.tf`). The post-install script is a Terraform template (`terraform/scripts/network-init.sh.tpl`) rendered per-router via `templatefile()`, matching each private interface to its expected subnet deterministically instead of guessing - it already handles any interface count, no hardcoded assumption of 2. `terraform/versions.tf` now pins the provider source (`vk-cs/vkcs`, `~> 0.17`), which was previously undeclared. +**Default security group** + +Every VK Cloud project auto-creates a `default` security group with a UUID unique to that project. Rather than hardcoding one project's UUID, it's resolved dynamically via `data.vkcs_networking_secgroup` (matched by `name = "default"`) and attached to every VM through `local.default_security_group_id`. `default_security_group_id` is a Terraform variable for the rare case that lookup doesn't fit a given project (non-standard name/SDN) - treat setting it explicitly (via `terraform.tfvars` or `TF_VAR_default_security_group_id`) as a last resort, not the normal path. + **Horizontal scaling via environment variables** `router_count` has a default and isn't set in `terraform.tfvars`, so it scales purely through `TF_VAR_router_count` using Terraform's standard `TF_VAR_` convention. `private_network_cidrs` has no default and must be set somewhere - either in `terraform.tfvars` (as shipped) or overridden via `TF_VAR_private_network_cidrs` as a JSON-encoded list: diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 93e0a1f..3104db5 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -26,6 +26,8 @@ private_network_cidrs = [ `private_network_cidrs` — обязательная переменная без значения по умолчанию: один CIDR-префикс на каждую приватную сеть, к которой будут подключены интерфейсы роутеров (один префикс = одна общая сеть = один приватный интерфейс на роутер). Автоматической нарезки нет — префиксы не должны пересекаться и должны вмещать минимум `router_count + 2` адреса. +UUID системной Security Group `default` (уникален для каждого проекта VK Cloud) вычисляется автоматически через `data.vkcs_networking_secgroup`, вручную задавать не нужно. Переменная `default_security_group_id` — override только на крайний случай (нестандартное имя/SDN группы в проекте), не для обычного использования. + ## 3. (Опционально) масштабирование По умолчанию: 2 роутера × 3 интерфейса (1 публичный + 2 приватных, по числу префиксов в `private_network_cidrs`). Меняется без правки кода — либо через `terraform.tfvars`, либо через переменные окружения: diff --git a/docs/changes/2026-09-04-dynamic-default-secgroup-plan.md b/docs/changes/2026-09-04-dynamic-default-secgroup-plan.md new file mode 100644 index 0000000..d00bd1e --- /dev/null +++ b/docs/changes/2026-09-04-dynamic-default-secgroup-plan.md @@ -0,0 +1,28 @@ +# План внедрения: динамическое вычисление UUID Default Security Group + +Дата: 2026-09-04 + +## Проблема + +В `terraform/main.tf` в 4 местах (router, priv_srv_01/02/03) захардкожен литерал `"d479b4d7-55b3-4ff1-bf8d-24d826a38f11"` — UUID системной Security Group `default`, автоматически создаваемой в каждом проекте VK Cloud, но **уникальной по UUID для каждого проекта**. Код не переносим между проектами без ручной правки. + +## Решение + +Провайдер `vkcs` предоставляет data source `vkcs_networking_secgroup` (проверено локально через `terraform providers schema -json` на уже установленном provider-mirror v0.17.2) с фильтром по `name`/`sdn` и computed-атрибутом `id`. Автосозданная группа по умолчанию называется `default` (подтверждено пользователем для целевого проекта). + +Явная передача UUID через переменную (в т.ч. `TF_VAR_default_security_group_id`) остаётся только как fallback на крайний случай — через `coalesce()`. + +## Шаги + +1. `terraform/main.tf`: добавить `data "vkcs_networking_secgroup" "default" { name = "default"; sdn = "sprut" }` и `locals.default_security_group_id = coalesce(var.default_security_group_id, data.vkcs_networking_secgroup.default.id)`; заменить 4 вхождения литерала на `local.default_security_group_id`. +2. `terraform/variables.tf`: добавить `default_security_group_id` (string, default `null`, без validation — это осознанный override). +3. `terraform/tests/test_terraform_delivery.py`: regression-guard на отсутствие старого UUID-литерала; проверка, что data source и локаль существуют и используются; расширить `variables.tf`-only plan-тест на новую переменную. +4. `README.md`/`docs/QUICKSTART.md`: кратко описать механизм и его fallback-статус. +5. Summary-документ по завершении. + +## Верификация + +- `terraform fmt -check -recursive`. +- Реальные `terraform init`/`validate` через локальный provider-mirror. +- `venv/bin/pytest terraform/tests -v`. +- Реальный lookup (`plan`/`apply` с настоящими credentials) проверяется пользователем самостоятельно в VK Cloud — вне офлайн-песочницы. diff --git a/docs/changes/2026-09-04-dynamic-default-secgroup-summary.md b/docs/changes/2026-09-04-dynamic-default-secgroup-summary.md new file mode 100644 index 0000000..6f58aa3 --- /dev/null +++ b/docs/changes/2026-09-04-dynamic-default-secgroup-summary.md @@ -0,0 +1,34 @@ +# Summary: динамическое вычисление UUID Default Security Group + +Дата: 2026-09-04 +План: [2026-09-04-dynamic-default-secgroup-plan.md](2026-09-04-dynamic-default-secgroup-plan.md) + +## Что сделано + +### `terraform/main.tf` +Добавлен `data "vkcs_networking_secgroup" "default" { name = "default"; sdn = "sprut" }` и `locals.default_security_group_id = coalesce(var.default_security_group_id, data.vkcs_networking_secgroup.default.id)`. Все 4 вхождения захардкоженного литерала `"d479b4d7-55b3-4ff1-bf8d-24d826a38f11"` (в `security_group_ids` роутера и `priv_srv_01/02/03`) заменены на `local.default_security_group_id`. + +Точное имя автосозданной группы (`default`, нижний регистр) подтверждено пользователем для целевого проекта. Data source и его атрибуты (`id` актуален, `secgroup_id` deprecated) проверены локально через `terraform providers schema -json` на уже установленном filesystem-mirror провайдера v0.17.2 — без обращения к реальному облаку. + +### `terraform/variables.tf` +Добавлена `default_security_group_id` (string, default `null`, без validation) — explicit override как крайняя мера; при `null` (значение по умолчанию) используется динамический lookup. + +### `terraform/tests/test_terraform_delivery.py` +Добавлены хелперы `find_local()` (main.tf теперь содержит два отдельных блока `locals {}`, поэтому старое обращение по индексу `main["locals"][0]` было неверным — исправлено) и `find_data_sources()`. Новые тесты: +- `test_default_security_group_data_source_exists` — data source объявлен с `name = "default"`; +- `test_default_security_group_id_local_prefers_override_then_lookup` — точная проверка выражения `coalesce(...)`; +- `test_no_hardcoded_security_group_uuid_in_main` — regression-guard: в `main.tf` не должно быть ни одного литерала в форме UUID; +- `test_all_instances_use_default_security_group_local` — все `vkcs_compute_instance` ссылаются на `local.default_security_group_id`, а не на литерал; +- `test_default_security_group_id_override_accepted` — реальный `terraform plan` на изолированном `variables.tf` принимает и `null` (по умолчанию), и явный override. + +Итог сьюта: **38 passed** (было 32). + +### `README.md` / `docs/QUICKSTART.md` +Добавлено краткое описание механизма и явная пометка override-переменной как "крайняя мера". + +## Верификация + +- `terraform fmt -check -recursive` → чисто. +- Реальные `terraform init`/`validate` (локальный provider-mirror) — с override и без → оба успешны. +- `venv/bin/pytest terraform/tests -v` → **38 passed**. +- Реальный lookup данных (`vkcs_networking_secgroup.default` резолвится в существующую группу проекта) можно проверить только `terraform plan`/`apply` с настоящими credentials в самом VK Cloud — вне рамок этой офлайн-песочницы; это отдельный шаг для пользователя при первом реальном деплое. diff --git a/terraform/main.tf b/terraform/main.tf index 8a71a5a..efefd84 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -12,6 +12,22 @@ data "vkcs_networking_network" "extnet" { sdn = "sprut" } +# Every VK Cloud project auto-creates a "default" security group with a +# UUID unique to that project - resolve it dynamically instead of +# hardcoding one project's UUID. var.default_security_group_id is a +# last-resort manual override (e.g. non-standard name/SDN in a project). +data "vkcs_networking_secgroup" "default" { + name = "default" + sdn = "sprut" +} + +locals { + default_security_group_id = coalesce( + var.default_security_group_id, + data.vkcs_networking_secgroup.default.id + ) +} + # LAN Network resource "vkcs_networking_network" "lan_net" { name = "router-lan-net" @@ -164,7 +180,7 @@ resource "vkcs_compute_instance" "router" { security_group_ids = [ vkcs_networking_secgroup.router_sg.id, - "d479b4d7-55b3-4ff1-bf8d-24d826a38f11" + local.default_security_group_id ] config_drive = true @@ -213,7 +229,7 @@ resource "vkcs_compute_instance" "priv_srv_01" { security_group_ids = [ vkcs_networking_secgroup.private_sg.id, - "d479b4d7-55b3-4ff1-bf8d-24d826a38f11" + local.default_security_group_id ] network { @@ -241,7 +257,7 @@ resource "vkcs_compute_instance" "priv_srv_02" { security_group_ids = [ vkcs_networking_secgroup.private_sg.id, - "d479b4d7-55b3-4ff1-bf8d-24d826a38f11" + local.default_security_group_id ] network { @@ -268,7 +284,7 @@ resource "vkcs_compute_instance" "priv_srv_03" { security_group_ids = [ vkcs_networking_secgroup.private_sg.id, - "d479b4d7-55b3-4ff1-bf8d-24d826a38f11" + local.default_security_group_id ] network { diff --git a/terraform/tests/test_terraform_delivery.py b/terraform/tests/test_terraform_delivery.py index 5fccdd9..64f2037 100644 --- a/terraform/tests/test_terraform_delivery.py +++ b/terraform/tests/test_terraform_delivery.py @@ -75,6 +75,21 @@ def find_variable(doc, name): return None +def find_local(doc, name): + """main.tf has more than one `locals { ... }` block - search all of them.""" + for block in doc.get("locals", []): + if name in block: + return block[name] + return None + + +def find_data_sources(doc, dtype): + """Yield (name, attrs) for every data source of a given type in a parsed doc.""" + for block in doc.get("data", []): + if dtype in block: + yield from block[dtype].items() + + # --------------------------------------------------------------------------- # Delivery layout # --------------------------------------------------------------------------- @@ -251,6 +266,21 @@ def test_router_count_validation_is_enforced(count, should_pass): assert ok == should_pass, f"unexpected result for router_count={count}:\n{output}" +@pytest.mark.parametrize( + "override,should_pass", + [ + (None, True), # default null - no override, dynamic lookup is used + ("11111111-1111-1111-1111-111111111111", True), # explicit override accepted + ], +) +def test_default_security_group_id_override_accepted(override, should_pass): + overrides = {} + if override is not None: + overrides["TF_VAR_default_security_group_id"] = override + ok, output = _plan_variables_only(overrides) + assert ok == should_pass, f"unexpected result for default_security_group_id={override!r}:\n{output}" + + # --------------------------------------------------------------------------- # HCL parses cleanly # --------------------------------------------------------------------------- @@ -343,8 +373,8 @@ def test_no_legacy_hardcoded_router_resources(): def test_private_roles_local_driven_by_variable(): main = load_tf("main.tf") - locals_block = main["locals"][0] - assert locals_block["private_roles"] == [ + private_roles = find_local(main, "private_roles") + assert private_roles == [ '${[for idx in range(length(var.private_network_cidrs)) : "priv${idx + 1}"]}' ], "locals.private_roles must be generated from length(var.private_network_cidrs)" @@ -359,6 +389,54 @@ def test_router_network_blocks_scale_with_private_roles(): ) +# --------------------------------------------------------------------------- +# main.tf: the "default" security group UUID is resolved dynamically, not +# hardcoded (it's unique per VK Cloud project) +# --------------------------------------------------------------------------- + + +def test_default_security_group_data_source_exists(): + main = load_tf("main.tf") + secgroups = dict(find_data_sources(main, "vkcs_networking_secgroup")) + assert "default" in secgroups, "expected data.vkcs_networking_secgroup.default" + assert secgroups["default"]["name"] == ["default"] + + +def test_default_security_group_id_local_prefers_override_then_lookup(): + main = load_tf("main.tf") + value = find_local(main, "default_security_group_id") + assert value == [ + "${coalesce(var.default_security_group_id," + "data.vkcs_networking_secgroup.default.id)}" + ], ( + "locals.default_security_group_id must fall back to the dynamic " + "lookup unless var.default_security_group_id is explicitly overridden" + ) + + +def test_no_hardcoded_security_group_uuid_in_main(): + text = (TERRAFORM_DIR / "main.tf").read_text() + uuid_pattern = re.compile( + r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", re.I + ) + assert not uuid_pattern.search(text), ( + "main.tf must not contain a literal UUID (e.g. a hardcoded security " + "group ID) - it's unique per project and must be resolved dynamically " + "via data.vkcs_networking_secgroup or overridden via " + "var.default_security_group_id" + ) + + +def test_all_instances_use_default_security_group_local(): + main = load_tf("main.tf") + for name, attrs in find_resources(main, "vkcs_compute_instance"): + sg_ids = attrs["security_group_ids"][0] + assert "${local.default_security_group_id}" in sg_ids, ( + f"vkcs_compute_instance.{name} does not reference " + f"local.default_security_group_id in security_group_ids" + ) + + # --------------------------------------------------------------------------- # terraform.tfvars example CIDRs: sanity-check the values actually shipped # (no auto-carving anymore - these come straight from the admin/example) diff --git a/terraform/variables.tf b/terraform/variables.tf index 5a9f358..cc426a6 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -61,3 +61,9 @@ variable "private_network_cidrs" { error_message = "private_network_cidrs entries must be unique." } } + +variable "default_security_group_id" { + description = "Explicit override for the project's 'default' security group UUID. Last resort only - by default it's resolved dynamically via data.vkcs_networking_secgroup (see main.tf), since this UUID is unique per project and must not be hardcoded." + type = string + default = null +}